tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 54from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74 75class TinkoffBrokerServer: 76 """ 77 This class implements methods to work with Tinkoff broker server. 78 79 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 80 81 About `token`: https://tinkoff.github.io/investAPI/token/ 82 """ 83 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 84 """ 85 Main class init. 86 87 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 88 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 89 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 90 :param useCache: use default cache file with raw data to use instead of `iList`. 91 True by default. Cache is auto-update if new day has come. 92 If you don't want to use cache and always updates raw data then set `useCache=False`. 93 :param defaultCache: path to default cache file. `dump.json` by default. 94 """ 95 if token is None or not token: 96 try: 97 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 98 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 99 100 except KeyError: 101 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 102 raise Exception("Token required") 103 104 else: 105 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 106 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 107 108 if accountId is None or not accountId: 109 try: 110 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 111 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 112 113 except KeyError: 114 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 115 116 else: 117 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 118 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 119 120 self.version = __version__ # duplicate here used TKSBrokerAPI main version 121 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 122 123 Latest version: https://pypi.org/project/tksbrokerapi/ 124 """ 125 126 self.aliases = TKS_TICKER_ALIASES 127 """Some aliases instead official tickers. 128 129 See also: `TKSEnums.TKS_TICKER_ALIASES` 130 """ 131 132 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 133 134 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 135 136 self.ticker = "" 137 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 138 139 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 140 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 141 142 See also: `SearchByTicker()`, `SearchInstruments()`. 143 """ 144 145 self.figi = "" 146 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 147 148 See also: `SearchByFIGI()`, `SearchInstruments()`. 149 """ 150 151 self.depth = 1 152 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 153 154 See also: `GetCurrentPrices()`. 155 """ 156 157 self.server = r"https://invest-public-api.tinkoff.ru/rest" 158 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 159 160 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 161 """ 162 163 uLogger.debug("Broker API server: {}".format(self.server)) 164 165 self.timeout = 15 166 """Server operations timeout in seconds. Default: `15`. 167 168 See also: `SendAPIRequest()`. 169 """ 170 171 self.headers = { 172 "Content-Type": "application/json", 173 "accept": "application/json", 174 "Authorization": "Bearer {}".format(self.token), 175 "x-app-name": "Tim55667757.TKSBrokerAPI", 176 } 177 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 178 179 See also: `SendAPIRequest()`. 180 """ 181 182 self.body = None 183 """Request body which send to broker server. Default: `None`. 184 185 See also: `SendAPIRequest()`. 186 """ 187 188 self.moreDebug = False 189 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 190 191 self.historyFile = None 192 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 193 194 See also: `History()`. 195 """ 196 197 self.htmlHistoryFile = "index.html" 198 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 199 200 See also: `ShowHistoryChart()`. 201 """ 202 203 self.instrumentsFile = "instruments.md" 204 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 205 206 See also: `ShowInstrumentsInfo()`. 207 """ 208 209 self.searchResultsFile = "search-results.md" 210 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 211 212 See also: `SearchInstruments()`. 213 """ 214 215 self.pricesFile = "prices.md" 216 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 217 218 See also: `GetListOfPrices()`. 219 """ 220 221 self.infoFile = "info.md" 222 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 223 224 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 225 """ 226 227 self.bondsXLSXFile = "ext-bonds.xlsx" 228 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 229 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 230 231 See also: `ExtendBondsData()`. 232 """ 233 234 self.calendarFile = "calendar.md" 235 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 236 237 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 238 239 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 240 """ 241 242 self.overviewFile = "overview.md" 243 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 244 245 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 246 """ 247 248 self.overviewDigestFile = "overview-digest.md" 249 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 250 251 See also: `Overview()` with parameter `details="digest"`. 252 """ 253 254 self.overviewPositionsFile = "overview-positions.md" 255 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 256 257 See also: `Overview()` with parameter `details="positions"`. 258 """ 259 260 self.overviewOrdersFile = "overview-orders.md" 261 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 262 263 See also: `Overview()` with parameter `details="orders"`. 264 """ 265 266 self.overviewAnalyticsFile = "overview-analytics.md" 267 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 268 269 See also: `Overview()` with parameter `details="analytics"`. 270 """ 271 272 self.overviewBondsCalendarFile = "overview-calendar.md" 273 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 274 275 See also: `Overview()` with parameter `details="calendar"`. 276 """ 277 278 self.reportFile = "deals.md" 279 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 280 281 See also: `Deals()`. 282 """ 283 284 self.withdrawalLimitsFile = "limits.md" 285 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 286 287 See also: `OverviewLimits()` and `RequestLimits()`. 288 """ 289 290 self.userInfoFile = "user-info.md" 291 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 292 293 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 294 """ 295 296 self.userAccountsFile = "accounts.md" 297 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 298 299 See also: `OverviewAccounts()`, `RequestAccounts()`. 300 """ 301 302 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 303 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 304 305 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 306 307 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 308 """ 309 310 self.iList = None # init iList for raw instruments data 311 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 312 313 See also: `Listing()`, `DumpInstruments()`. 314 """ 315 316 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 317 if useCache: 318 if os.path.exists(self.iListDumpFile): 319 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 320 curTime = datetime.now(tzutc()) 321 322 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 323 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 324 325 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 326 327 else: 328 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 329 330 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 331 os.path.abspath(self.iListDumpFile), 332 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 333 )) 334 335 else: 336 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 337 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 338 339 else: 340 self.iList = self.Listing() # request new raw instruments data from broker server 341 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 342 343 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 344 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 345 346 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 347 """ 348 349 def _ParseJSON(self, rawData="{}") -> dict: 350 """ 351 Parse JSON from response string. 352 353 :param rawData: this is a string with JSON-formatted text. 354 :return: JSON (dictionary), parsed from server response string. 355 """ 356 responseJSON = json.loads(rawData) if rawData else {} 357 358 if self.moreDebug: 359 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 360 361 return responseJSON 362 363 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 364 """ 365 Send GET or POST request to broker server and receive JSON object. 366 367 self.header: must be defining with dictionary of headers. 368 self.body: if define then used as request body. None by default. 369 self.timeout: global request timeout, 15 seconds by default. 370 :param url: url with REST request. 371 :param reqType: send "GET" or "POST" request. "GET" by default. 372 :param retry: how many times retry after first request if an 5xx server errors occurred. 373 :param pause: sleep time in seconds between retries. 374 :return: response JSON (dictionary) from broker. 375 """ 376 if reqType not in ("GET", "POST"): 377 uLogger.error("You can define request type: 'GET' or 'POST'!") 378 raise Exception("Incorrect value") 379 380 if self.moreDebug: 381 uLogger.debug("Request parameters:") 382 uLogger.debug(" - REST API URL: {}".format(url)) 383 uLogger.debug(" - request type: {}".format(reqType)) 384 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 385 uLogger.debug(" - body:\n{}".format(self.body)) 386 387 # fast hack to avoid all operations with some tickers/FIGI 388 responseJSON = {} 389 oK = True 390 for item in self.exclude: 391 if item in url: 392 if self.moreDebug: 393 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 394 395 oK = False 396 break 397 398 if oK: 399 counter = 0 400 response = None 401 errMsg = "" 402 403 while not response and counter <= retry: 404 if reqType == "GET": 405 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 406 407 if reqType == "POST": 408 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 409 410 if self.moreDebug: 411 uLogger.debug("Response:") 412 uLogger.debug(" - status code: {}".format(response.status_code)) 413 uLogger.debug(" - reason: {}".format(response.reason)) 414 uLogger.debug(" - body length: {}".format(len(response.text))) 415 uLogger.debug(" - headers:\n{}".format(response.headers)) 416 417 # Server returns some headers: 418 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 419 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 420 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 421 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 422 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 423 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 424 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 425 sleep(rateLimitWait) 426 427 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 428 if 400 <= response.status_code < 500: 429 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 430 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 431 counter = retry + 1 432 433 if 500 <= response.status_code < 600: 434 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 435 uLogger.debug(" - not oK, {}".format(errMsg)) 436 counter += 1 437 438 if counter <= retry: 439 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 440 sleep(pause) 441 442 responseJSON = self._ParseJSON(rawData=response.text) 443 444 if errMsg: 445 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 446 uLogger.error(" - not oK, {}".format(errMsg)) 447 448 return responseJSON 449 450 def _IUpdater(self, iType: str) -> tuple: 451 """ 452 Request instrument by type from server. See available API methods for instruments: 453 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 454 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 455 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 456 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 457 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 458 459 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 460 :return: tuple with iType name and list of available instruments of current type for defined user token. 461 """ 462 result = [] 463 464 if iType in TKS_INSTRUMENTS: 465 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 466 467 # all instruments have the same body in API v2 requests: 468 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 469 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 470 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 471 472 return iType, result 473 474 def _IWrapper(self, kwargs): 475 """ 476 Wrapper runs instrument's update method `_IUpdater()`. 477 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 478 """ 479 return self._IUpdater(**kwargs) 480 481 def Listing(self) -> dict: 482 """ 483 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 484 485 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 486 """ 487 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 488 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 489 490 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 491 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 492 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 493 494 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 495 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 496 poolUpdater.close() 497 498 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 499 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 500 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 501 502 # calculate minimum price increment (step) for all instruments and set up instrument's type: 503 for iType in iList.keys(): 504 for ticker in iList[iType]: 505 iList[iType][ticker]["type"] = iType 506 507 if "minPriceIncrement" in iList[iType][ticker].keys(): 508 iList[iType][ticker]["step"] = NanoToFloat( 509 iList[iType][ticker]["minPriceIncrement"]["units"], 510 iList[iType][ticker]["minPriceIncrement"]["nano"], 511 ) 512 513 else: 514 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 515 516 return iList 517 518 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 519 """ 520 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 521 522 See also: `DumpInstruments()`, `Listing()`. 523 524 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 525 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 526 """ 527 if self.iListDumpFile is None or not self.iListDumpFile: 528 uLogger.error("Output name of dump file must be defined!") 529 raise Exception("Filename required") 530 531 if not self.iList or forceUpdate: 532 self.iList = self.Listing() 533 534 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 535 536 # Save as XLSX with separated sheets for every type of instruments: 537 with pd.ExcelWriter( 538 path=xlsxDumpFile, 539 date_format=TKS_DATE_FORMAT, 540 datetime_format=TKS_DATE_TIME_FORMAT, 541 mode="w", 542 ) as writer: 543 for iType in TKS_INSTRUMENTS: 544 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 545 df = df[sorted(df)] # sorted by column names 546 df = df.applymap( 547 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 548 na_action="ignore", 549 ) # converting numbers from nano-type to float in every cell 550 df.to_excel( 551 writer, 552 sheet_name=iType, 553 encoding="UTF-8", 554 freeze_panes=(1, 1), 555 ) # saving as XLSX-file with freeze first row and column as headers 556 557 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 558 559 def DumpInstruments(self, forceUpdate: bool = True) -> str: 560 """ 561 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 562 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 563 564 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 565 566 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 567 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 568 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 569 """ 570 if self.iListDumpFile is None or not self.iListDumpFile: 571 uLogger.error("Output name of dump file must be defined!") 572 raise Exception("Filename required") 573 574 if not self.iList or forceUpdate: 575 self.iList = self.Listing() 576 577 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 578 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 579 fH.write(jsonDump) 580 581 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 582 583 return jsonDump 584 585 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 586 """ 587 Show information about one instrument defined by json data and prints it in Markdown format. 588 589 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 590 591 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 592 :param show: if `True` then also printing information about instrument and its current price. 593 :return: multilines text in Markdown format with information about one instrument. 594 """ 595 splitLine = "| | |\n" 596 infoText = "" 597 598 if iJSON is not None and iJSON and isinstance(iJSON, dict): 599 info = [ 600 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 601 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 602 "| Parameters | Values |\n", 603 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 604 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 605 "| Full name: | {:<54} |\n".format(iJSON["name"]), 606 ] 607 608 if "sector" in iJSON.keys() and iJSON["sector"]: 609 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 610 611 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 612 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 613 614 info.extend([ 615 splitLine, 616 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 617 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 618 ]) 619 620 if "isin" in iJSON.keys() and iJSON["isin"]: 621 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 622 623 if "classCode" in iJSON.keys(): 624 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 625 626 info.extend([ 627 splitLine, 628 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 629 splitLine, 630 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 631 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 632 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 633 ]) 634 635 if iJSON["figi"]: 636 self.figi = iJSON["figi"] 637 iJSON = iJSON | self.RequestTradingStatus() 638 639 info.extend([ 640 splitLine, 641 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 642 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 643 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 644 ]) 645 646 info.append(splitLine) 647 648 if "type" in iJSON.keys() and iJSON["type"]: 649 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 650 651 if "shareType" in iJSON.keys() and iJSON["shareType"]: 652 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 653 654 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 655 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 656 657 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 658 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 659 660 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 661 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 662 663 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 664 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 665 666 if "focusType" in iJSON.keys() and iJSON["focusType"]: 667 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 668 669 if "assetType" in iJSON.keys() and iJSON["assetType"]: 670 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 671 672 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 673 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 674 675 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 676 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 677 678 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 679 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 680 681 if "currency" in iJSON.keys(): 682 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 683 684 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 685 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 686 687 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 688 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 689 690 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 691 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 692 693 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 694 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 695 696 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 697 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 698 699 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 700 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 701 702 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 703 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 704 705 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 706 info.append("| Perpetual bond: | Yes |\n") 707 708 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 709 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 710 711 iExt = None 712 if iJSON["type"] == "Bonds": 713 info.extend([ 714 splitLine, 715 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 716 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 717 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 718 iJSON["nominal"]["currency"], 719 )), 720 ]) 721 722 if "floatingCouponFlag" in iJSON.keys(): 723 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 724 725 if "amortizationFlag" in iJSON.keys(): 726 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 727 728 info.append(splitLine) 729 730 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 731 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 732 733 if iJSON["figi"]: 734 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 735 736 info.extend([ 737 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 738 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 739 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 740 ]) 741 742 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 743 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 744 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 745 iJSON["aciValue"]["currency"] 746 ))) 747 748 if "currentPrice" in iJSON.keys(): 749 info.append(splitLine) 750 751 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 752 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 753 754 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 755 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 756 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 757 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 758 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 759 760 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 761 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 762 763 info.extend([ 764 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 765 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 766 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 767 )), 768 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 769 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 770 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 771 )), 772 "| Changes between last deal price and last close | {:<54} |\n".format( 773 "{:.2f}%{}".format( 774 iJSON["currentPrice"]["changes"], 775 " ({}{:.2f} {})".format( 776 "+" if bondChangesDelta > 0 else "", 777 bondChangesDelta, 778 aciCurrency 779 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 780 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 781 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 782 currency 783 ), 784 ) 785 ), 786 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 787 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 788 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 789 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 790 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 791 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 792 )), 793 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 794 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 795 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 796 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 797 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 798 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 799 )), 800 ]) 801 802 if "lot" in iJSON.keys(): 803 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 804 805 if "step" in iJSON.keys() and iJSON["step"] != 0: 806 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 807 808 # Add bond payment calendar: 809 if iJSON["type"] == "Bonds": 810 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 811 info.extend(["\n", strCalendar]) 812 813 infoText += "".join(info) 814 815 if show: 816 uLogger.info("{}".format(infoText)) 817 818 else: 819 uLogger.debug("{}".format(infoText)) 820 821 if self.infoFile is not None: 822 with open(self.infoFile, "w", encoding="UTF-8") as fH: 823 fH.write(infoText) 824 825 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 826 827 return infoText 828 829 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 830 """ 831 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 832 833 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 834 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 835 :return: JSON formatted data with information about instrument. 836 """ 837 tickerJSON = {} 838 if self.moreDebug: 839 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 840 841 if not self.ticker: 842 uLogger.warning("self.ticker variable is not be empty!") 843 844 else: 845 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 846 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 847 raise Exception("Instrument not allowed") 848 849 if not self.iList: 850 self.iList = self.Listing() 851 852 if self.ticker in self.iList["Shares"].keys(): 853 tickerJSON = self.iList["Shares"][self.ticker] 854 if self.moreDebug: 855 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 856 857 elif self.ticker in self.iList["Currencies"].keys(): 858 tickerJSON = self.iList["Currencies"][self.ticker] 859 if self.moreDebug: 860 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 861 862 elif self.ticker in self.iList["Bonds"].keys(): 863 tickerJSON = self.iList["Bonds"][self.ticker] 864 if self.moreDebug: 865 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 866 867 elif self.ticker in self.iList["Etfs"].keys(): 868 tickerJSON = self.iList["Etfs"][self.ticker] 869 if self.moreDebug: 870 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 871 872 elif self.ticker in self.iList["Futures"].keys(): 873 tickerJSON = self.iList["Futures"][self.ticker] 874 if self.moreDebug: 875 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 876 877 if tickerJSON: 878 self.figi = tickerJSON["figi"] 879 880 if requestPrice: 881 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 882 883 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 884 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 885 886 else: 887 tickerJSON["currentPrice"]["changes"] = 0 888 889 if show: 890 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 891 892 else: 893 if show: 894 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 895 896 return tickerJSON 897 898 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 899 """ 900 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 901 902 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 903 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 904 :return: JSON formatted data with information about instrument. 905 """ 906 figiJSON = {} 907 if self.moreDebug: 908 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 909 910 if not self.figi: 911 uLogger.warning("self.figi variable is not be empty!") 912 913 else: 914 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 915 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 916 raise Exception("Instrument not allowed") 917 918 if not self.iList: 919 self.iList = self.Listing() 920 921 for item in self.iList["Shares"].keys(): 922 if self.figi == self.iList["Shares"][item]["figi"]: 923 figiJSON = self.iList["Shares"][item] 924 925 if self.moreDebug: 926 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 927 928 break 929 930 if not figiJSON: 931 for item in self.iList["Currencies"].keys(): 932 if self.figi == self.iList["Currencies"][item]["figi"]: 933 figiJSON = self.iList["Currencies"][item] 934 935 if self.moreDebug: 936 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 937 938 break 939 940 if not figiJSON: 941 for item in self.iList["Bonds"].keys(): 942 if self.figi == self.iList["Bonds"][item]["figi"]: 943 figiJSON = self.iList["Bonds"][item] 944 945 if self.moreDebug: 946 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 947 948 break 949 950 if not figiJSON: 951 for item in self.iList["Etfs"].keys(): 952 if self.figi == self.iList["Etfs"][item]["figi"]: 953 figiJSON = self.iList["Etfs"][item] 954 955 if self.moreDebug: 956 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 957 958 break 959 960 if not figiJSON: 961 for item in self.iList["Futures"].keys(): 962 if self.figi == self.iList["Futures"][item]["figi"]: 963 figiJSON = self.iList["Futures"][item] 964 965 if self.moreDebug: 966 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 967 968 break 969 970 if figiJSON: 971 self.figi = figiJSON["figi"] 972 self.ticker = figiJSON["ticker"] 973 974 if requestPrice: 975 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 976 977 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 978 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 979 980 else: 981 figiJSON["currentPrice"]["changes"] = 0 982 983 if show: 984 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 985 986 else: 987 if show: 988 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 989 990 return figiJSON 991 992 def GetCurrentPrices(self, show: bool = True) -> dict: 993 """ 994 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 995 `{"buy": [{"price": 1243.8, "quantity": 193}, 996 {"price": 1244.0, "quantity": 168}, 997 {"price": 1244.8, "quantity": 5}, 998 {"price": 1245.0, "quantity": 61}, 999 {"price": 1245.4, "quantity": 60}], 1000 "sell": [{"price": 1243.6, "quantity": 8}, 1001 {"price": 1242.6, "quantity": 10}, 1002 {"price": 1242.4, "quantity": 18}, 1003 {"price": 1242.2, "quantity": 50}, 1004 {"price": 1242.0, "quantity": 113}], 1005 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1006 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1007 - sell: list of dicts with Buyers prices, 1008 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1009 - quantity: volume value by current price in lots, 1010 - limitUp: current trade session limit price, maximum, 1011 - limitDown: current trade session limit price, minimum, 1012 - lastPrice: last deal price of the instrument, 1013 - closePrice: previous trade session close price of the instrument. 1014 1015 See also: `SearchByTicker()` and `SearchByFIGI()`. 1016 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1017 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1018 1019 :param show: if `True` then print DOM to log and console. 1020 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1021 If an error occurred then returns an empty record: 1022 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1023 """ 1024 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1025 1026 if self.depth < 1: 1027 uLogger.error("Depth of Market (DOM) must be >=1!") 1028 raise Exception("Incorrect value") 1029 1030 if not (self.ticker or self.figi): 1031 uLogger.error("self.ticker or self.figi variables must be defined!") 1032 raise Exception("Ticker or FIGI required") 1033 1034 if self.ticker and not self.figi: 1035 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1036 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1037 1038 if not self.ticker and self.figi: 1039 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1040 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1041 1042 if not self.figi: 1043 uLogger.error("FIGI is not defined!") 1044 raise Exception("Ticker or FIGI required") 1045 1046 else: 1047 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1048 1049 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1050 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1051 self.body = str({"figi": self.figi, "depth": self.depth}) 1052 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1053 1054 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1055 # list of dicts with sellers orders: 1056 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1057 1058 # list of dicts with buyers orders: 1059 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1060 1061 # max price of instrument at this time: 1062 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1063 1064 # min price of instrument at this time: 1065 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1066 1067 # last price of deal with instrument: 1068 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1069 1070 # last close price of instrument: 1071 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1072 1073 else: 1074 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1075 uLogger.debug("Server response: {}".format(pricesResponse)) 1076 1077 if show: 1078 if prices["buy"] or prices["sell"]: 1079 info = [ 1080 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1081 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1082 self.ticker, 1083 self.figi, 1084 self.depth, 1085 ), 1086 "-" * 60, "\n", 1087 " Orders of Buyers | Orders of Sellers\n", 1088 "-" * 60, "\n", 1089 " Sell prices (volumes) | Buy prices (volumes)\n", 1090 "-" * 60, "\n", 1091 ] 1092 1093 if not prices["buy"]: 1094 info.append(" | No orders!\n") 1095 sumBuy = 0 1096 1097 else: 1098 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1099 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1100 for item in maxMinSorted: 1101 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1102 1103 if not prices["sell"]: 1104 info.append("No orders! |\n") 1105 sumSell = 0 1106 1107 else: 1108 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1109 for item in prices["sell"]: 1110 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1111 1112 info.extend([ 1113 "-" * 60, "\n", 1114 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1115 "-" * 60, "\n", 1116 ]) 1117 1118 infoText = "".join(info) 1119 1120 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1121 1122 else: 1123 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1124 1125 return prices 1126 1127 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1128 """ 1129 This method get and show information about all available broker instruments for current user account. 1130 If `instrumentsFile` string is not empty then also save information to this file. 1131 1132 :param show: if `True` then print results to console, if `False` — print only to file. 1133 :return: multi-lines string with all available broker instruments 1134 """ 1135 if not self.iList: 1136 self.iList = self.Listing() 1137 1138 info = [ 1139 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1140 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1141 ] 1142 1143 # add instruments count by type: 1144 for iType in self.iList.keys(): 1145 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1146 1147 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1148 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1149 1150 # generating info tables with all instruments by type: 1151 for iType in self.iList.keys(): 1152 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1153 1154 for instrument in self.iList[iType].keys(): 1155 iName = self.iList[iType][instrument]["name"] # instrument's name 1156 if len(iName) > 57: 1157 iName = "{}...".format(iName[:54]) # right trim for a long string 1158 1159 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1160 self.iList[iType][instrument]["ticker"], 1161 iName, 1162 self.iList[iType][instrument]["figi"], 1163 self.iList[iType][instrument]["currency"], 1164 self.iList[iType][instrument]["lot"], 1165 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1166 )) 1167 1168 infoText = "".join(info) 1169 1170 if show: 1171 uLogger.info(infoText) 1172 1173 if self.instrumentsFile: 1174 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1175 fH.write(infoText) 1176 1177 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1178 1179 return infoText 1180 1181 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1182 """ 1183 This method search and show information about instruments by part of its ticker, FIGI or name. 1184 If `searchResultsFile` string is not empty then also save information to this file. 1185 1186 :param pattern: string with part of ticker, FIGI or instrument's name. 1187 :param show: if `True` then print results to console, if `False` — return list of result only. 1188 :return: list of dictionaries with all found instruments. 1189 """ 1190 if not self.iList: 1191 self.iList = self.Listing() 1192 1193 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1194 compiledPattern = re.compile(pattern, re.IGNORECASE) 1195 1196 for iType in self.iList: 1197 for instrument in self.iList[iType].values(): 1198 searchResult = compiledPattern.search(" ".join( 1199 [instrument["ticker"], instrument["figi"], instrument["name"]] 1200 )) 1201 1202 if searchResult: 1203 searchResults[iType][instrument["ticker"]] = instrument 1204 1205 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1206 info = [ 1207 "# Search results\n\n", 1208 "* **Search pattern:** [{}]\n".format(pattern), 1209 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1210 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1211 ] 1212 infoShort = info[:] 1213 1214 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1215 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1216 skippedLine = "| ... | ... | ... | ... |\n" 1217 1218 if resultsLen == 0: 1219 info.append("\nNo results\n") 1220 infoShort.append("\nNo results\n") 1221 uLogger.warning("No results. Try changing your search pattern.") 1222 1223 else: 1224 for iType in searchResults: 1225 iTypeValuesCount = len(searchResults[iType].values()) 1226 if iTypeValuesCount > 0: 1227 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1228 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1229 1230 for instrument in searchResults[iType].values(): 1231 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1232 instrument["type"], 1233 instrument["ticker"], 1234 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1235 instrument["figi"], 1236 )) 1237 1238 if iTypeValuesCount <= 5: 1239 infoShort.extend(info[-iTypeValuesCount:]) 1240 1241 else: 1242 infoShort.extend(info[-5:]) 1243 infoShort.append(skippedLine) 1244 1245 infoText = "".join(info) 1246 infoTextShort = "".join(infoShort) 1247 1248 if show: 1249 uLogger.info(infoTextShort) 1250 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1251 1252 if self.searchResultsFile: 1253 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1254 fH.write(infoText) 1255 1256 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1257 1258 return searchResults 1259 1260 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1261 """ 1262 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1263 1264 :param instruments: list of strings with tickers or FIGIs. 1265 :return: list with unique instrument FIGIs only. 1266 """ 1267 requestedInstruments = [] 1268 for iName in instruments: 1269 if iName not in self.aliases.keys(): 1270 if iName not in requestedInstruments: 1271 requestedInstruments.append(iName) 1272 1273 else: 1274 if iName not in requestedInstruments: 1275 if self.aliases[iName] not in requestedInstruments: 1276 requestedInstruments.append(self.aliases[iName]) 1277 1278 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1279 1280 onlyUniqueFIGIs = [] 1281 for iName in requestedInstruments: 1282 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1283 continue 1284 1285 self.ticker = iName 1286 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1287 1288 if not iData: 1289 self.ticker = "" 1290 self.figi = iName 1291 1292 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1293 1294 if not iData: 1295 self.figi = "" 1296 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1297 1298 if iData and iData["figi"] not in onlyUniqueFIGIs: 1299 onlyUniqueFIGIs.append(iData["figi"]) 1300 1301 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1302 1303 return onlyUniqueFIGIs 1304 1305 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1306 """ 1307 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1308 1309 See limits: https://tinkoff.github.io/investAPI/limits/ 1310 1311 If `pricesFile` string is not empty then also save information to this file. 1312 1313 :param instruments: list of strings with tickers or FIGIs. 1314 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1315 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1316 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1317 """ 1318 if instruments is None or not instruments: 1319 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1320 raise Exception("Ticker or FIGI required") 1321 1322 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1323 1324 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1325 1326 iList = [] # trying to get info and current prices about all unique instruments: 1327 for self.figi in onlyUniqueFIGIs: 1328 iData = self.SearchByFIGI(requestPrice=True) 1329 iList.append(iData) 1330 1331 self.ShowListOfPrices(iList, show) 1332 1333 return iList 1334 1335 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1336 """ 1337 Show table contains current prices of given instruments. 1338 1339 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1340 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1341 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1342 :return: multilines text in Markdown format as a table contains current prices. 1343 """ 1344 infoText = "" 1345 1346 if show or self.pricesFile: 1347 info = [ 1348 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1349 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1350 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1351 ] 1352 1353 for item in iList: 1354 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1355 item["ticker"], 1356 item["figi"], 1357 item["type"], 1358 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1359 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1360 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1361 "{} / {}".format( 1362 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1363 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1364 ), 1365 "{} / {}".format( 1366 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1367 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1368 ), 1369 item["currency"], 1370 )) 1371 1372 infoText = "".join(info) 1373 1374 if show: 1375 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1376 1377 if self.pricesFile: 1378 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1379 fH.write(infoText) 1380 1381 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1382 1383 return infoText 1384 1385 def RequestTradingStatus(self) -> dict: 1386 """ 1387 Requesting trading status for the instrument defined by `figi` variable. 1388 1389 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1390 1391 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1392 1393 :return: dictionary with trading status attributes. Response example: 1394 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1395 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1396 """ 1397 if self.figi is None or not self.figi: 1398 uLogger.error("Variable `figi` must be defined for using this method!") 1399 raise Exception("FIGI required") 1400 1401 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1402 1403 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1404 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1405 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1406 1407 if self.moreDebug: 1408 uLogger.debug("Records about current trading status successfully received") 1409 1410 return tradingStatus 1411 1412 def RequestPortfolio(self) -> dict: 1413 """ 1414 Requesting actual user's portfolio for current `accountId`. 1415 1416 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1417 1418 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1419 1420 :return: dictionary with user's portfolio. 1421 """ 1422 if self.accountId is None or not self.accountId: 1423 uLogger.error("Variable `accountId` must be defined for using this method!") 1424 raise Exception("Account ID required") 1425 1426 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1427 1428 self.body = str({"accountId": self.accountId}) 1429 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1430 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1431 1432 if self.moreDebug: 1433 uLogger.debug("Records about user's portfolio successfully received") 1434 1435 return rawPortfolio 1436 1437 def RequestPositions(self) -> dict: 1438 """ 1439 Requesting open positions by currencies and instruments for current `accountId`. 1440 1441 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1442 1443 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1444 1445 :return: dictionary with open positions by instruments. 1446 """ 1447 if self.accountId is None or not self.accountId: 1448 uLogger.error("Variable `accountId` must be defined for using this method!") 1449 raise Exception("Account ID required") 1450 1451 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1452 1453 self.body = str({"accountId": self.accountId}) 1454 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1455 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1456 1457 if self.moreDebug: 1458 uLogger.debug("Records about current open positions successfully received") 1459 1460 return rawPositions 1461 1462 def RequestPendingOrders(self) -> list: 1463 """ 1464 Requesting current actual pending orders for current `accountId`. 1465 1466 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1467 1468 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1469 1470 :return: list of dictionaries with pending orders. 1471 """ 1472 if self.accountId is None or not self.accountId: 1473 uLogger.error("Variable `accountId` must be defined for using this method!") 1474 raise Exception("Account ID required") 1475 1476 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1477 1478 self.body = str({"accountId": self.accountId}) 1479 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1480 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1481 1482 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1483 1484 return rawOrders 1485 1486 def RequestStopOrders(self) -> list: 1487 """ 1488 Requesting current actual stop orders for current `accountId`. 1489 1490 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1491 1492 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1493 1494 :return: list of dictionaries with stop orders. 1495 """ 1496 if self.accountId is None or not self.accountId: 1497 uLogger.error("Variable `accountId` must be defined for using this method!") 1498 raise Exception("Account ID required") 1499 1500 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1501 1502 self.body = str({"accountId": self.accountId}) 1503 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1504 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1505 1506 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1507 1508 return rawStopOrders 1509 1510 def Overview(self, show: bool = False, details: str = "full") -> dict: 1511 """ 1512 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1513 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1514 and `overviewBondsCalendarFile` are defined then also save information to file. 1515 1516 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1517 many requests about the state of the portfolio, and then, based on the received data, a large number 1518 of calculation and statistics are collected. 1519 1520 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1521 :param details: how detailed should the information be? 1522 - `full` — shows full available information about portfolio status (by default), 1523 - `positions` — shows only open positions, 1524 - `orders` — shows only sections of open limits and stop orders. 1525 - `digest` — show a short digest of the portfolio status, 1526 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1527 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1528 :return: dictionary with client's raw portfolio and some statistics. 1529 """ 1530 if self.accountId is None or not self.accountId: 1531 uLogger.error("Variable `accountId` must be defined for using this method!") 1532 raise Exception("Account ID required") 1533 1534 view = { 1535 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1536 "headers": {}, # list of dictionaries, response headers without "positions" section 1537 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1538 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1539 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1540 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1541 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1542 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1543 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1544 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1545 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1546 }, 1547 "stat": { # --- some statistics calculated using "raw" sections: 1548 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1549 "availableRUB": 0., # available rubles (without other currencies) 1550 "blockedRUB": 0., # blocked sum in Russian Rouble 1551 "totalChangesRUB": 0., # changes for all open trades in RUB 1552 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1553 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1554 "sharesCostRUB": 0., # costs of all shares in RUB 1555 "bondsCostRUB": 0., # costs of all bonds in RUB 1556 "etfsCostRUB": 0., # costs of all etfs in RUB 1557 "futuresCostRUB": 0., # costs of all futures in RUB 1558 "Currencies": [], # list of dictionaries of all currencies statistics 1559 "Shares": [], # list of dictionaries of all shares statistics 1560 "Bonds": [], # list of dictionaries of all bonds statistics 1561 "Etfs": [], # list of dictionaries of all etfs statistics 1562 "Futures": [], # list of dictionaries of all futures statistics 1563 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1564 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1565 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1566 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1567 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1568 }, 1569 "analytics": { # --- some analytics of portfolio: 1570 "distrByAssets": {}, # portfolio distribution by assets 1571 "distrByCompanies": {}, # portfolio distribution by companies 1572 "distrBySectors": {}, # portfolio distribution by sectors 1573 "distrByCurrencies": {}, # portfolio distribution by currencies 1574 "distrByCountries": {}, # portfolio distribution by countries 1575 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1576 } 1577 } 1578 1579 details = details.lower() 1580 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1581 if details not in availableDetails: 1582 details = "full" 1583 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1584 1585 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1586 1587 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1588 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1589 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1590 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1591 1592 # save response headers without "positions" section: 1593 for key in portfolioResponse.keys(): 1594 if key != "positions": 1595 view["raw"]["headers"][key] = portfolioResponse[key] 1596 1597 else: 1598 continue 1599 1600 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1601 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1602 for item in portfolioResponse["positions"]: 1603 if item["instrumentType"] == "currency": 1604 self.figi = item["figi"] 1605 curr = self.SearchByFIGI(requestPrice=False) 1606 1607 # current price of currency in RUB: 1608 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1609 "name": curr["name"], 1610 "currentPrice": NanoToFloat( 1611 item["currentPrice"]["units"], 1612 item["currentPrice"]["nano"] 1613 ), 1614 } 1615 1616 view["raw"]["Currencies"].append(item) 1617 1618 elif item["instrumentType"] == "share": 1619 view["raw"]["Shares"].append(item) 1620 1621 elif item["instrumentType"] == "bond": 1622 view["raw"]["Bonds"].append(item) 1623 1624 elif item["instrumentType"] == "etf": 1625 view["raw"]["Etfs"].append(item) 1626 1627 elif item["instrumentType"] == "futures": 1628 view["raw"]["Futures"].append(item) 1629 1630 else: 1631 continue 1632 1633 # how many volume of currencies (by ISO currency name) are blocked: 1634 for item in view["raw"]["positions"]["blocked"]: 1635 blocked = NanoToFloat(item["units"], item["nano"]) 1636 if blocked > 0: 1637 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1638 1639 # how many volume of instruments (by FIGI) are blocked: 1640 for item in view["raw"]["positions"]["securities"]: 1641 blocked = int(item["blocked"]) 1642 if blocked > 0: 1643 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1644 1645 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1646 1647 if "rub" in allBlocked.keys(): 1648 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1649 1650 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1651 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1652 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1653 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1654 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1655 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1656 view["stat"]["portfolioCostRUB"] = sum([ 1657 view["stat"]["allCurrenciesCostRUB"], 1658 view["stat"]["sharesCostRUB"], 1659 view["stat"]["bondsCostRUB"], 1660 view["stat"]["etfsCostRUB"], 1661 view["stat"]["futuresCostRUB"], 1662 ]) 1663 1664 # --- calculating some portfolio statistics: 1665 byComp = {} # distribution by companies 1666 bySect = {} # distribution by sectors 1667 byCurr = {} # distribution by currencies (include RUB) 1668 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1669 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1670 1671 for item in portfolioResponse["positions"]: 1672 self.figi = item["figi"] 1673 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1674 1675 if instrument: 1676 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1677 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1678 1679 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1680 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1681 1682 else: 1683 blocked = 0 1684 1685 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1686 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1687 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1688 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1689 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1690 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1691 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1692 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1693 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1694 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1695 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1696 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1697 1698 statData = { 1699 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1700 "ticker": instrument["ticker"], # ticker by FIGI 1701 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1702 "volume": volume, # available volume of instrument 1703 "lots": lots, # volume in lots of instrument 1704 "direction": direction, # direction of an instrument's position: short or long 1705 "blocked": blocked, # blocked volume of currency or instrument 1706 "currentPrice": curPrice, # current instrument's price in basic asset 1707 "average": average, # current average position price 1708 "cost": cost, # current cost of all volume of instrument in basic asset 1709 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1710 "costRUB": costRUB, # cost of instrument in ruble 1711 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1712 "profit": profit, # expected profit at current moment 1713 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1714 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1715 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1716 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1717 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1718 "step": instrument["step"], # minimum price increment 1719 } 1720 1721 # adding distribution by unique countries: 1722 if statData["country"] not in byCountry.keys(): 1723 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1724 1725 else: 1726 byCountry[statData["country"]]["cost"] += costRUB 1727 byCountry[statData["country"]]["percent"] += percentCostRUB 1728 1729 if item["instrumentType"] != "currency": 1730 # adding distribution by unique companies: 1731 if statData["name"]: 1732 if statData["name"] not in byComp.keys(): 1733 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1734 1735 else: 1736 byComp[statData["name"]]["cost"] += costRUB 1737 byComp[statData["name"]]["percent"] += percentCostRUB 1738 1739 # adding distribution by unique sectors: 1740 if statData["sector"] not in bySect.keys(): 1741 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1742 1743 else: 1744 bySect[statData["sector"]]["cost"] += costRUB 1745 bySect[statData["sector"]]["percent"] += percentCostRUB 1746 1747 # adding distribution by unique currencies: 1748 if currency not in byCurr.keys(): 1749 byCurr[currency] = { 1750 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1751 "cost": costRUB, 1752 "percent": percentCostRUB 1753 } 1754 1755 else: 1756 byCurr[currency]["cost"] += costRUB 1757 byCurr[currency]["percent"] += percentCostRUB 1758 1759 # saving statistics for every instrument: 1760 if item["instrumentType"] == "currency": 1761 view["stat"]["Currencies"].append(statData) 1762 1763 # update dict with free funds for trading (total - blocked) by currencies 1764 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1765 view["stat"]["funds"][currency] = { 1766 "total": volume, 1767 "totalCostRUB": costRUB, # total volume cost in rubles 1768 "free": volume - blocked, 1769 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1770 } 1771 1772 elif item["instrumentType"] == "share": 1773 view["stat"]["Shares"].append(statData) 1774 1775 elif item["instrumentType"] == "bond": 1776 view["stat"]["Bonds"].append(statData) 1777 1778 elif item["instrumentType"] == "etf": 1779 view["stat"]["Etfs"].append(statData) 1780 1781 elif item["instrumentType"] == "Futures": 1782 view["stat"]["Futures"].append(statData) 1783 1784 else: 1785 continue 1786 1787 # total changes in Russian Ruble: 1788 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1789 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1790 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1791 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1792 view["stat"]["funds"]["rub"] = { 1793 "total": view["stat"]["availableRUB"], 1794 "totalCostRUB": view["stat"]["availableRUB"], 1795 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1796 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1797 } 1798 1799 # --- pending orders sector data: 1800 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1801 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1802 1803 for item in view["raw"]["orders"]: 1804 self.figi = item["figi"] 1805 1806 if item["figi"] not in uniquePendingOrdersFIGIs: 1807 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1808 1809 uniquePendingOrdersFIGIs.append(item["figi"]) 1810 uniquePendingOrders[item["figi"]] = instrument 1811 1812 else: 1813 instrument = uniquePendingOrders[item["figi"]] 1814 1815 if instrument: 1816 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1817 orderType = TKS_ORDER_TYPES[item["orderType"]] 1818 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1819 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1820 1821 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1822 if item["direction"] == "ORDER_DIRECTION_BUY": 1823 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1824 1825 else: 1826 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1827 1828 # requested price for order execution: 1829 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1830 1831 # necessary changes in percent to reach target from current price: 1832 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1833 1834 view["stat"]["orders"].append({ 1835 "orderID": item["orderId"], # orderId number parameter of current order 1836 "figi": item["figi"], # FIGI identification 1837 "ticker": instrument["ticker"], # ticker name by FIGI 1838 "lotsRequested": item["lotsRequested"], # requested lots value 1839 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1840 "currentPrice": lastPrice, # current instrument's price for defined action 1841 "targetPrice": target, # requested price for order execution in base currency 1842 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1843 "percentChanges": changes, # changes in percent to target from current price 1844 "currency": item["currency"], # instrument's currency name 1845 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1846 "type": orderType, # type of order from TKS_ORDER_TYPES 1847 "status": orderState, # order status from TKS_ORDER_STATES 1848 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1849 }) 1850 1851 # --- stop orders sector data: 1852 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1853 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1854 1855 for item in view["raw"]["stopOrders"]: 1856 self.figi = item["figi"] 1857 1858 if item["figi"] not in uniqueStopOrdersFIGIs: 1859 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1860 1861 uniqueStopOrdersFIGIs.append(item["figi"]) 1862 uniqueStopOrders[item["figi"]] = instrument 1863 1864 else: 1865 instrument = uniqueStopOrders[item["figi"]] 1866 1867 if instrument: 1868 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1869 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1870 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1871 1872 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1873 if "expirationTime" in item.keys(): 1874 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1875 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1876 1877 else: 1878 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1879 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1880 1881 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1882 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1883 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1884 1885 else: 1886 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1887 1888 # requested price when stop-order executed: 1889 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1890 1891 # price for limit-order, set up when stop-order executed: 1892 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1893 1894 # necessary changes in percent to reach target from current price: 1895 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1896 1897 view["stat"]["stopOrders"].append({ 1898 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1899 "figi": item["figi"], # FIGI identification 1900 "ticker": instrument["ticker"], # ticker name by FIGI 1901 "lotsRequested": item["lotsRequested"], # requested lots value 1902 "currentPrice": lastPrice, # current instrument's price for defined action 1903 "targetPrice": target, # requested price for stop-order execution in base currency 1904 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1905 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1906 "percentChanges": changes, # changes in percent to target from current price 1907 "currency": item["currency"], # instrument's currency name 1908 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1909 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1910 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1911 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1912 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1913 }) 1914 1915 # --- calculating data for analytics section: 1916 # portfolio distribution by assets: 1917 view["analytics"]["distrByAssets"] = { 1918 "Ruble": { 1919 "uniques": 1, 1920 "cost": view["stat"]["availableRUB"], 1921 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1922 }, 1923 "Currencies": { 1924 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1925 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1926 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1927 }, 1928 "Shares": { 1929 "uniques": len(view["stat"]["Shares"]), 1930 "cost": view["stat"]["sharesCostRUB"], 1931 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1932 }, 1933 "Bonds": { 1934 "uniques": len(view["stat"]["Bonds"]), 1935 "cost": view["stat"]["bondsCostRUB"], 1936 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1937 }, 1938 "Etfs": { 1939 "uniques": len(view["stat"]["Etfs"]), 1940 "cost": view["stat"]["etfsCostRUB"], 1941 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1942 }, 1943 "Futures": { 1944 "uniques": len(view["stat"]["Futures"]), 1945 "cost": view["stat"]["futuresCostRUB"], 1946 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1947 }, 1948 } 1949 1950 # portfolio distribution by companies: 1951 view["analytics"]["distrByCompanies"]["All money cash"] = { 1952 "ticker": "", 1953 "cost": view["stat"]["allCurrenciesCostRUB"], 1954 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1955 } 1956 view["analytics"]["distrByCompanies"].update(byComp) 1957 1958 # portfolio distribution by sectors: 1959 view["analytics"]["distrBySectors"]["All money cash"] = { 1960 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1961 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1962 } 1963 view["analytics"]["distrBySectors"].update(bySect) 1964 1965 # portfolio distribution by currencies: 1966 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1967 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1968 1969 if self.moreDebug: 1970 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1971 1972 view["analytics"]["distrByCurrencies"].update(byCurr) 1973 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1974 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1975 1976 # portfolio distribution by countries: 1977 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1978 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1979 1980 if self.moreDebug: 1981 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1982 1983 view["analytics"]["distrByCountries"].update(byCountry) 1984 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1985 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1986 1987 # --- Prepare text statistics overview in human-readable: 1988 if show: 1989 # Whatever the value `details`, header not changes: 1990 info = [ 1991 "# Client's portfolio\n\n", 1992 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1993 "* **Account ID:** [{}]\n".format(self.accountId), 1994 ] 1995 1996 if details in ["full", "positions", "digest"]: 1997 info.extend([ 1998 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 1999 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2000 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2001 view["stat"]["totalChangesRUB"], 2002 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2003 view["stat"]["totalChangesPercentRUB"], 2004 ), 2005 ]) 2006 2007 if details in ["full", "positions"]: 2008 info.extend([ 2009 "## Open positions\n\n", 2010 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2011 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2012 "| Ruble | {:>31} | | | | | |\n".format( 2013 "{:.2f} ({:.2f}) rub".format( 2014 view["stat"]["availableRUB"], 2015 view["stat"]["blockedRUB"], 2016 ) 2017 ) 2018 ]) 2019 2020 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2021 return [ 2022 "| | | | | | | |\n", 2023 "| {:<27} | | | | | {:>19} | |\n".format( 2024 noTradeStr if noTradeStr else typeStr, 2025 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2026 ), 2027 ] 2028 2029 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2030 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2031 "{} [{}]".format(data["ticker"], data["figi"]), 2032 "{:.2f} ({:.2f}) {}".format( 2033 data["volume"], 2034 data["blocked"], 2035 data["currency"], 2036 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2037 data["volume"], 2038 data["blocked"], 2039 ), 2040 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2041 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2042 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2043 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2044 "{}{:.2f} {} ({}{:.2f}%)".format( 2045 "+" if data["profit"] > 0 else "", 2046 data["profit"], data["baseCurrencyName"], 2047 "+" if data["percentProfit"] > 0 else "", 2048 data["percentProfit"], 2049 ), 2050 ) 2051 2052 # --- Show currencies section: 2053 if view["stat"]["Currencies"]: 2054 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2055 for item in view["stat"]["Currencies"]: 2056 info.append(_InfoStr(item, showCurrencyName=True)) 2057 2058 else: 2059 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2060 2061 # --- Show shares section: 2062 if view["stat"]["Shares"]: 2063 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2064 2065 for item in view["stat"]["Shares"]: 2066 info.append(_InfoStr(item)) 2067 2068 else: 2069 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2070 2071 # --- Show bonds section: 2072 if view["stat"]["Bonds"]: 2073 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2074 2075 for item in view["stat"]["Bonds"]: 2076 info.append(_InfoStr(item)) 2077 2078 else: 2079 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2080 2081 # --- Show etfs section: 2082 if view["stat"]["Etfs"]: 2083 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2084 2085 for item in view["stat"]["Etfs"]: 2086 info.append(_InfoStr(item)) 2087 2088 else: 2089 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2090 2091 # --- Show futures section: 2092 if view["stat"]["Futures"]: 2093 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2094 2095 for item in view["stat"]["Futures"]: 2096 info.append(_InfoStr(item)) 2097 2098 else: 2099 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2100 2101 if details in ["full", "orders"]: 2102 # --- Show pending orders section: 2103 if view["stat"]["orders"]: 2104 info.extend([ 2105 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2106 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2107 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2108 ]) 2109 2110 for item in view["stat"]["orders"]: 2111 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2112 "{} [{}]".format(item["ticker"], item["figi"]), 2113 item["orderID"], 2114 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2115 "{} {} ({}{:.2f}%)".format( 2116 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2117 item["baseCurrencyName"], 2118 "+" if item["percentChanges"] > 0 else "", 2119 float(item["percentChanges"]), 2120 ), 2121 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2122 item["action"], 2123 item["type"], 2124 item["date"], 2125 )) 2126 2127 else: 2128 info.append("\n## Total pending limit-orders: 0\n") 2129 2130 # --- Show stop orders section: 2131 if view["stat"]["stopOrders"]: 2132 info.extend([ 2133 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2134 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2135 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2136 ]) 2137 2138 for item in view["stat"]["stopOrders"]: 2139 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2140 "{} [{}]".format(item["ticker"], item["figi"]), 2141 item["orderID"], 2142 item["lotsRequested"], 2143 "{} {} ({}{:.2f}%)".format( 2144 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2145 item["baseCurrencyName"], 2146 "+" if item["percentChanges"] > 0 else "", 2147 float(item["percentChanges"]), 2148 ), 2149 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2150 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2151 item["action"], 2152 item["type"], 2153 item["expType"], 2154 item["createDate"], 2155 item["expDate"], 2156 )) 2157 2158 else: 2159 info.append("\n## Total stop-orders: 0\n") 2160 2161 if details in ["full", "analytics"]: 2162 # -- Show analytics section: 2163 if view["stat"]["portfolioCostRUB"] > 0: 2164 info.extend([ 2165 "\n# Analytics\n" 2166 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2167 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2168 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2169 view["stat"]["totalChangesRUB"], 2170 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2171 view["stat"]["totalChangesPercentRUB"], 2172 ), 2173 "\n## Portfolio distribution by assets\n" 2174 "\n| Type | Uniques | Percent | Current cost |\n", 2175 "|------------------------------------|---------|---------|--------------------|\n", 2176 ]) 2177 2178 for key in view["analytics"]["distrByAssets"].keys(): 2179 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2180 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2181 key, 2182 view["analytics"]["distrByAssets"][key]["uniques"], 2183 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2184 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2185 )) 2186 2187 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2188 2189 info.extend([ 2190 "\n## Portfolio distribution by companies\n" 2191 "\n| Company | Percent | Current cost |\n", 2192 aSepLine, 2193 ]) 2194 2195 for company in view["analytics"]["distrByCompanies"].keys(): 2196 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2197 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2198 "{}{}".format( 2199 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2200 company, 2201 ), 2202 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2203 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2204 )) 2205 2206 info.extend([ 2207 "\n## Portfolio distribution by sectors\n" 2208 "\n| Sector | Percent | Current cost |\n", 2209 aSepLine, 2210 ]) 2211 2212 for sector in view["analytics"]["distrBySectors"].keys(): 2213 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2214 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2215 sector, 2216 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2217 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2218 )) 2219 2220 info.extend([ 2221 "\n## Portfolio distribution by currencies\n" 2222 "\n| Instruments currencies | Percent | Current cost |\n", 2223 aSepLine, 2224 ]) 2225 2226 for curr in view["analytics"]["distrByCurrencies"].keys(): 2227 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2228 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2229 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2230 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2231 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2232 )) 2233 2234 info.extend([ 2235 "\n## Portfolio distribution by countries\n" 2236 "\n| Assets by country | Percent | Current cost |\n", 2237 aSepLine, 2238 ]) 2239 2240 for country in view["analytics"]["distrByCountries"].keys(): 2241 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2242 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2243 country, 2244 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2245 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2246 )) 2247 2248 if details in ["full", "calendar"]: 2249 # -- Show bonds payment calendar section: 2250 if view["stat"]["Bonds"]: 2251 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2252 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2253 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2254 2255 else: 2256 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2257 2258 infoText = "".join(info) 2259 2260 uLogger.info(infoText) 2261 2262 if details == "full" and self.overviewFile: 2263 filename = self.overviewFile 2264 2265 elif details == "digest" and self.overviewDigestFile: 2266 filename = self.overviewDigestFile 2267 2268 elif details == "positions" and self.overviewPositionsFile: 2269 filename = self.overviewPositionsFile 2270 2271 elif details == "orders" and self.overviewOrdersFile: 2272 filename = self.overviewOrdersFile 2273 2274 elif details == "analytics" and self.overviewAnalyticsFile: 2275 filename = self.overviewAnalyticsFile 2276 2277 elif details == "calendar" and self.overviewBondsCalendarFile: 2278 filename = self.overviewBondsCalendarFile 2279 2280 else: 2281 filename = "" 2282 2283 if filename: 2284 with open(filename, "w", encoding="UTF-8") as fH: 2285 fH.write(infoText) 2286 2287 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2288 2289 return view 2290 2291 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2292 """ 2293 Returns history operations between two given dates for current `accountId`. 2294 If `reportFile` string is not empty then also save human-readable report. 2295 Shows some statistical data of closed positions. 2296 2297 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2298 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2299 :param show: if `True` then also prints all records to the console. 2300 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2301 :return: original list of dictionaries with history of deals records from API ("operations" key): 2302 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2303 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2304 """ 2305 if self.accountId is None or not self.accountId: 2306 uLogger.error("Variable `accountId` must be defined for using this method!") 2307 raise Exception("Account ID required") 2308 2309 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2310 2311 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2312 2313 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2314 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2315 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2316 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2317 customStat = {} # custom statistics in additional to responseJSON 2318 2319 # --- output report in human-readable format: 2320 if show or self.reportFile: 2321 splitLine1 = "| | | | | |\n" # Summary section 2322 splitLine2 = "| | | | | | | | |\n" # Operations section 2323 nextDay = "" 2324 2325 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2326 2327 if len(ops) > 0: 2328 customStat = { 2329 "opsCount": 0, # total operations count 2330 "buyCount": 0, # buy operations 2331 "sellCount": 0, # sell operations 2332 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2333 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2334 "payIn": {"rub": 0.}, # Deposit brokerage account 2335 "payOut": {"rub": 0.}, # Withdrawals 2336 "divs": {"rub": 0.}, # Dividends income 2337 "coupons": {"rub": 0.}, # Coupon's income 2338 "brokerCom": {"rub": 0.}, # Service commissions 2339 "serviceCom": {"rub": 0.}, # Service commissions 2340 "marginCom": {"rub": 0.}, # Margin commissions 2341 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2342 } 2343 2344 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2345 for item in ops: 2346 if item["state"] == "OPERATION_STATE_EXECUTED": 2347 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2348 2349 # count buy operations: 2350 if "_BUY" in item["operationType"]: 2351 customStat["buyCount"] += 1 2352 2353 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2354 customStat["buyTotal"][item["payment"]["currency"]] += payment 2355 2356 else: 2357 customStat["buyTotal"][item["payment"]["currency"]] = payment 2358 2359 # count sell operations: 2360 elif "_SELL" in item["operationType"]: 2361 customStat["sellCount"] += 1 2362 2363 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2364 customStat["sellTotal"][item["payment"]["currency"]] += payment 2365 2366 else: 2367 customStat["sellTotal"][item["payment"]["currency"]] = payment 2368 2369 # count incoming operations: 2370 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2371 if item["payment"]["currency"] in customStat["payIn"].keys(): 2372 customStat["payIn"][item["payment"]["currency"]] += payment 2373 2374 else: 2375 customStat["payIn"][item["payment"]["currency"]] = payment 2376 2377 # count withdrawals operations: 2378 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2379 if item["payment"]["currency"] in customStat["payOut"].keys(): 2380 customStat["payOut"][item["payment"]["currency"]] += payment 2381 2382 else: 2383 customStat["payOut"][item["payment"]["currency"]] = payment 2384 2385 # count dividends income: 2386 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2387 if item["payment"]["currency"] in customStat["divs"].keys(): 2388 customStat["divs"][item["payment"]["currency"]] += payment 2389 2390 else: 2391 customStat["divs"][item["payment"]["currency"]] = payment 2392 2393 # count coupon's income: 2394 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2395 if item["payment"]["currency"] in customStat["coupons"].keys(): 2396 customStat["coupons"][item["payment"]["currency"]] += payment 2397 2398 else: 2399 customStat["coupons"][item["payment"]["currency"]] = payment 2400 2401 # count broker commissions: 2402 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2403 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2404 customStat["brokerCom"][item["payment"]["currency"]] += payment 2405 2406 else: 2407 customStat["brokerCom"][item["payment"]["currency"]] = payment 2408 2409 # count service commissions: 2410 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2411 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2412 customStat["serviceCom"][item["payment"]["currency"]] += payment 2413 2414 else: 2415 customStat["serviceCom"][item["payment"]["currency"]] = payment 2416 2417 # count margin commissions: 2418 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2419 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2420 customStat["marginCom"][item["payment"]["currency"]] += payment 2421 2422 else: 2423 customStat["marginCom"][item["payment"]["currency"]] = payment 2424 2425 # count withholding taxes: 2426 elif "_TAX" in item["operationType"]: 2427 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2428 customStat["allTaxes"][item["payment"]["currency"]] += payment 2429 2430 else: 2431 customStat["allTaxes"][item["payment"]["currency"]] = payment 2432 2433 else: 2434 continue 2435 2436 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2437 2438 # --- view "Actions" lines: 2439 info.extend([ 2440 "| Report sections | | | | |\n", 2441 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2442 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2443 "| | Buy: {:<22} | {:<28} | | |\n".format( 2444 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2445 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2446 ), 2447 "| | Sell: {:<21} | {:<28} | | |\n".format( 2448 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2449 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2450 ), 2451 ]) 2452 2453 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2454 for key in opsKeys: 2455 if key == "rub": 2456 continue 2457 2458 info.extend([ 2459 "| | | {:<28} | | |\n".format( 2460 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2461 ), 2462 "| | | {:<28} | | |\n".format( 2463 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2464 ), 2465 ]) 2466 2467 info.append(splitLine1) 2468 2469 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2470 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2471 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2472 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2473 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2474 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2475 ) 2476 2477 # --- view "Payments" lines: 2478 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2479 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2480 2481 for key in paymentsKeys: 2482 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2483 2484 info.append(splitLine1) 2485 2486 # --- view "Commissions and taxes" lines: 2487 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2488 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2489 2490 for key in comKeys: 2491 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2492 2493 info.append(splitLine1) 2494 2495 info.extend([ 2496 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2497 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2498 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2499 ]) 2500 2501 else: 2502 info.append("Broker returned no operations during this period\n") 2503 2504 # --- view "Operations" section: 2505 for item in ops: 2506 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2507 continue 2508 2509 else: 2510 self.figi = item["figi"] if item["figi"] else "" 2511 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2512 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2513 2514 # group of deals during one day: 2515 if nextDay and item["date"].split("T")[0] != nextDay: 2516 info.append(splitLine2) 2517 nextDay = "" 2518 2519 else: 2520 nextDay = item["date"].split("T")[0] # saving current day for splitting 2521 2522 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2523 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2524 self.figi if self.figi else "—", 2525 instrument["ticker"] if instrument else "—", 2526 instrument["type"] if instrument else "—", 2527 item["quantity"] if int(item["quantity"]) > 0 else "—", 2528 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2529 TKS_OPERATION_STATES[item["state"]], 2530 TKS_OPERATION_TYPES[item["operationType"]], 2531 )) 2532 2533 infoText = "".join(info) 2534 2535 if show: 2536 if self.moreDebug: 2537 uLogger.debug("Records about history of a client's operations successfully received") 2538 2539 uLogger.info(infoText) 2540 2541 if self.reportFile: 2542 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2543 fH.write(infoText) 2544 2545 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2546 2547 return ops, customStat 2548 2549 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2550 """ 2551 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2552 2553 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2554 Warning! Broker server used ISO UTC time by default. 2555 2556 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2557 Also, `historyFile` used to update history with `onlyMissing` parameter. 2558 2559 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2560 2561 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2562 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2563 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2564 `"hour"`, `"day"`. Default: `"hour"`. 2565 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2566 False by default. Warning! History appends only from last candle to current time 2567 with always update last candle! 2568 :param csvSep: separator if csv-file is used, `,` by default. 2569 :param show: if `True` then also prints Pandas DataFrame to the console. 2570 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2571 `["date", "time", "open", "high", "low", "close", "volume"]`. 2572 """ 2573 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2574 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2575 history = None # empty pandas object for history 2576 2577 if interval not in TKS_CANDLE_INTERVALS.keys(): 2578 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2579 raise Exception("Incorrect value") 2580 2581 if not (self.ticker or self.figi): 2582 uLogger.error("Ticker or FIGI must be defined!") 2583 raise Exception("Ticker or FIGI required") 2584 2585 if self.ticker and not self.figi: 2586 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2587 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2588 2589 if self.figi and not self.ticker: 2590 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2591 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2592 2593 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2594 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2595 if interval.lower() != "day": 2596 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2597 2598 delta = dtEnd - dtStart # current UTC time minus last time in file 2599 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2600 2601 # calculate history length in candles: 2602 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2603 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2604 length += 1 # to avoid fraction time 2605 2606 # calculate data blocks count: 2607 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2608 2609 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2610 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2611 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2612 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2613 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2614 2615 tempOld = None # pandas object for old history, if --only-missing key present 2616 lastTime = None # datetime object of last old candle in file 2617 2618 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2619 uLogger.debug("--only-missing key present, add only last missing candles...") 2620 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2621 2622 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2623 2624 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2625 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2626 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2627 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2628 2629 # get last datetime object from last string in file or minus 1 delta if file is empty: 2630 if len(tempOld) > 0: 2631 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2632 2633 else: 2634 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2635 2636 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2637 2638 responseJSONs = [] # raw history blocks of data 2639 2640 blockEnd = dtEnd 2641 for item in range(blocks): 2642 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2643 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2644 2645 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2646 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2647 )) 2648 2649 if blockStart == blockEnd: 2650 uLogger.debug("Skipped this zero-length block...") 2651 2652 else: 2653 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2654 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2655 self.body = str({ 2656 "figi": self.figi, 2657 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2658 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2659 "interval": TKS_CANDLE_INTERVALS[interval][0] 2660 }) 2661 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2662 2663 if "code" in responseJSON.keys(): 2664 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2665 2666 else: 2667 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2668 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2669 2670 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2671 2672 blockEnd = blockStart 2673 2674 printCount = len(responseJSONs) # candles to show in console 2675 if responseJSONs: 2676 tempHistory = pd.DataFrame( 2677 data={ 2678 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2679 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2680 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2681 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2682 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2683 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2684 "volume": [int(item["volume"]) for item in responseJSONs], 2685 }, 2686 index=range(len(responseJSONs)), 2687 columns=["date", "time", "open", "high", "low", "close", "volume"], 2688 ) 2689 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2690 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2691 2692 # append only newest candles to old history if --only-missing key present: 2693 if onlyMissing and tempOld is not None and lastTime is not None: 2694 index = 0 # find start index in tempHistory data: 2695 2696 for i, item in tempHistory.iterrows(): 2697 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2698 2699 if curTime == lastTime: 2700 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2701 index = i 2702 printCount = index + 1 2703 break 2704 2705 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2706 2707 else: 2708 history = tempHistory # if no `--only-missing` key then load full data from server 2709 2710 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2711 2712 if history is not None and not history.empty: 2713 if show: 2714 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2715 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2716 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2717 )) 2718 2719 else: 2720 uLogger.warning("Received an empty candles history!") 2721 2722 if self.historyFile is not None: 2723 if history is not None and not history.empty: 2724 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2725 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2726 2727 else: 2728 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2729 2730 else: 2731 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2732 2733 return history 2734 2735 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2736 """ 2737 Load candles history from csv-file and return Pandas DataFrame object. 2738 2739 See also: `History()` and `ShowHistoryChart()` methods. 2740 2741 :param filePath: path to csv-file to open. 2742 """ 2743 loadedHistory = None # init candles data object 2744 2745 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2746 2747 if os.path.exists(filePath): 2748 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2749 2750 tfStr = self.priceModel.FormattedDelta( 2751 self.priceModel.timeframe, 2752 "{days} days {hours}h {minutes}m {seconds}s", 2753 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2754 self.priceModel.timeframe, 2755 "{hours}h {minutes}m {seconds}s", 2756 ) 2757 2758 if loadedHistory is not None and not loadedHistory.empty: 2759 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2760 len(loadedHistory), 2761 tfStr, 2762 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2763 ) 2764 2765 else: 2766 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2767 2768 else: 2769 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2770 2771 return loadedHistory 2772 2773 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2774 """ 2775 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2776 2777 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2778 Default: `index.html` (both for interact and non-interact candlesticks chart). 2779 2780 See also: `History()` and `LoadHistory()` methods. 2781 2782 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2783 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2784 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2785 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2786 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2787 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2788 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2789 """ 2790 if isinstance(candles, str): 2791 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2792 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2793 2794 elif isinstance(candles, pd.DataFrame): 2795 self.priceModel.prices = candles # set candles chain from variable 2796 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2797 2798 if "datetime" not in candles.columns: 2799 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2800 2801 else: 2802 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2803 raise Exception("Incorrect value") 2804 2805 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2806 2807 if interact: 2808 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2809 2810 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2811 2812 else: 2813 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2814 2815 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2816 2817 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2818 2819 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2820 """ 2821 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2822 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2823 2824 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2825 2826 :param operation: string "Buy" or "Sell". 2827 :param lots: volume, integer count of lots >= 1. 2828 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2829 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2830 :param expDate: string "Undefined" by default or local date in future, 2831 it is a string with format `%Y-%m-%d %H:%M:%S`. 2832 :return: JSON with response from broker server. 2833 """ 2834 if self.accountId is None or not self.accountId: 2835 uLogger.error("Variable `accountId` must be defined for using this method!") 2836 raise Exception("Account ID required") 2837 2838 if operation is None or not operation or operation not in ("Buy", "Sell"): 2839 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2840 raise Exception("Incorrect value") 2841 2842 if lots is None or lots < 1: 2843 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2844 lots = 1 2845 2846 if tp is None or tp < 0: 2847 tp = 0 2848 2849 if sl is None or sl < 0: 2850 sl = 0 2851 2852 if expDate is None or not expDate: 2853 expDate = "Undefined" 2854 2855 if not (self.ticker or self.figi): 2856 uLogger.error("Ticker or FIGI must be defined!") 2857 raise Exception("Ticker or FIGI required") 2858 2859 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2860 self.ticker = instrument["ticker"] 2861 self.figi = instrument["figi"] 2862 2863 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2864 2865 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2866 self.body = str({ 2867 "figi": self.figi, 2868 "quantity": str(lots), 2869 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2870 "accountId": str(self.accountId), 2871 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2872 }) 2873 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2874 2875 if "orderId" in response.keys(): 2876 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2877 operation, response["orderId"], 2878 self.ticker, self.figi, lots, 2879 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2880 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2881 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2882 )) 2883 2884 if tp > 0: 2885 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2886 2887 if sl > 0: 2888 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2889 2890 else: 2891 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2892 2893 return response 2894 2895 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2896 """ 2897 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2898 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2899 2900 See also: `Order()` and `Trade()` docstrings. 2901 2902 :param lots: volume, integer count of lots >= 1. 2903 :param tp: float > 0, take profit price of stop-order. 2904 :param sl: float > 0, stop loss price of stop-order. 2905 :param expDate: it's a local date in future. 2906 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2907 :return: JSON with response from broker server. 2908 """ 2909 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2910 2911 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2912 """ 2913 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2914 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2915 2916 See also: `Order()` and `Trade()` docstrings. 2917 2918 :param lots: volume, integer count of lots >= 1. 2919 :param tp: float > 0, take profit price of stop-order. 2920 :param sl: float > 0, stop loss price of stop-order. 2921 :param expDate: it's a local date in the future. 2922 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2923 :return: JSON with response from broker server. 2924 """ 2925 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2926 2927 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2928 """ 2929 Close position of given instruments. 2930 2931 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2932 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2933 This avoids unnecessary downloading data from the server. 2934 """ 2935 if instruments is None or not instruments: 2936 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2937 raise Exception("Ticker or FIGI required") 2938 2939 if isinstance(instruments, str): 2940 instruments = [instruments] 2941 2942 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2943 if uniqueInstruments: 2944 if portfolio is None or not portfolio: 2945 portfolio = self.Overview(show=False) 2946 2947 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2948 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2949 2950 for self.figi in uniqueInstruments: 2951 if self.figi not in allOpened: 2952 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2953 continue 2954 2955 # search open trade info about instrument by ticker: 2956 instrument = {} 2957 for iType in TKS_INSTRUMENTS: 2958 if instrument: 2959 break 2960 2961 for item in portfolio["stat"][iType]: 2962 if item["figi"] == self.figi: 2963 instrument = item 2964 break 2965 2966 if instrument: 2967 self.ticker = instrument["ticker"] 2968 self.figi = instrument["figi"] 2969 2970 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2971 self.ticker, 2972 self.figi, 2973 int(instrument["volume"]), 2974 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2975 )) 2976 2977 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2978 2979 if tradeLots > 0: 2980 if instrument["blocked"] > 0: 2981 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2982 instrument["blocked"], 2983 self.ticker, 2984 tradeLots, 2985 )) 2986 2987 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2988 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 2989 2990 else: 2991 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 2992 2993 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 2994 """ 2995 Close all positions of given instruments with defined type. 2996 2997 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 2998 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2999 This avoids unnecessary downloading data from the server. 3000 """ 3001 if iType not in TKS_INSTRUMENTS: 3002 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3003 3004 else: 3005 if portfolio is None or not portfolio: 3006 portfolio = self.Overview(show=False) 3007 3008 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3009 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3010 3011 if tickers and portfolio: 3012 self.CloseTrades(tickers, portfolio) 3013 3014 else: 3015 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3016 3017 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3018 """ 3019 Universal method to create market or limit orders with all available parameters for current `accountId`. 3020 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3021 3022 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3023 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3024 3025 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3026 then broker immediately open market order as you can do simple --buy or --sell operations! 3027 3028 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3029 When current price will go up or down to target price value then broker opens a limit order. 3030 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3031 3032 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3033 3034 :param operation: string "Buy" or "Sell". 3035 :param orderType: string "Limit" or "Stop". 3036 :param lots: volume, integer count of lots >= 1. 3037 :param targetPrice: target price > 0. This is open trade price for limit order. 3038 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3039 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3040 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3041 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3042 Stop loss order always executed by market price. 3043 :param expDate: string "Undefined" by default or local date in future. 3044 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3045 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3046 A limit order has no expiration date, it lasts until the end of the trading day. 3047 :return: JSON with response from broker server. 3048 """ 3049 if self.accountId is None or not self.accountId: 3050 uLogger.error("Variable `accountId` must be defined for using this method!") 3051 raise Exception("Account ID required") 3052 3053 if operation is None or not operation or operation not in ("Buy", "Sell"): 3054 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3055 raise Exception("Incorrect value") 3056 3057 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3058 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3059 raise Exception("Incorrect value") 3060 3061 if lots is None or lots < 1: 3062 uLogger.error("You must define trade volume > 0: integer count of lots!") 3063 raise Exception("Incorrect value") 3064 3065 if targetPrice is None or targetPrice <= 0: 3066 uLogger.error("Target price for limit-order must be greater than 0!") 3067 raise Exception("Incorrect value") 3068 3069 if limitPrice is None or limitPrice <= 0: 3070 limitPrice = targetPrice 3071 3072 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3073 stopType = "Limit" 3074 3075 if expDate is None or not expDate: 3076 expDate = "Undefined" 3077 3078 if not (self.ticker or self.figi): 3079 uLogger.error("Tocker or FIGI must be defined!") 3080 raise Exception("Ticker or FIGI required") 3081 3082 response = {} 3083 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3084 self.ticker = instrument["ticker"] 3085 self.figi = instrument["figi"] 3086 3087 if orderType == "Limit": 3088 uLogger.debug( 3089 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3090 self.ticker, self.figi, 3091 operation, lots, targetPrice, instrument["currency"], 3092 )) 3093 3094 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3095 self.body = str({ 3096 "figi": self.figi, 3097 "quantity": str(lots), 3098 "price": FloatToNano(targetPrice), 3099 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3100 "accountId": str(self.accountId), 3101 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3102 }) 3103 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3104 3105 if "orderId" in response.keys(): 3106 uLogger.info( 3107 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3108 response["orderId"], 3109 self.ticker, self.figi, 3110 operation, lots, targetPrice, instrument["currency"], 3111 )) 3112 3113 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3114 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3115 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3116 targetPrice, instrument["currency"], 3117 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3118 )) 3119 3120 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3121 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3122 targetPrice, instrument["currency"], 3123 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3124 )) 3125 3126 else: 3127 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3128 3129 if orderType == "Stop": 3130 uLogger.debug( 3131 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3132 self.ticker, self.figi, 3133 operation, lots, 3134 targetPrice, instrument["currency"], 3135 limitPrice, instrument["currency"], 3136 stopType, expDate, 3137 )) 3138 3139 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3140 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3141 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3142 3143 body = { 3144 "figi": self.figi, 3145 "quantity": str(lots), 3146 "price": FloatToNano(limitPrice), 3147 "stopPrice": FloatToNano(targetPrice), 3148 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3149 "accountId": str(self.accountId), 3150 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3151 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3152 } 3153 3154 if expDateUTC: 3155 body["expireDate"] = expDateUTC 3156 3157 self.body = str(body) 3158 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3159 3160 if "stopOrderId" in response.keys(): 3161 uLogger.info( 3162 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3163 response["stopOrderId"], 3164 self.ticker, self.figi, 3165 operation, lots, 3166 targetPrice, instrument["currency"], 3167 limitPrice, instrument["currency"], 3168 TKS_STOP_ORDER_TYPES[stopOrderType], 3169 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3170 )) 3171 3172 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3173 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3174 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3175 targetPrice, instrument["currency"], 3176 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3177 )) 3178 3179 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3180 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3181 targetPrice, instrument["currency"], 3182 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3183 )) 3184 3185 else: 3186 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3187 3188 return response 3189 3190 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3191 """ 3192 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3193 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3194 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3195 See also: `Order()` docstring. 3196 3197 :param lots: volume, integer count of lots >= 1. 3198 :param targetPrice: target price > 0. This is open trade price for limit order. 3199 :return: JSON with response from broker server. 3200 """ 3201 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3202 3203 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3204 """ 3205 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3206 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3207 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3208 target price value then broker opens a limit order. See also: `Order()` docstring. 3209 3210 :param lots: volume, integer count of lots >= 1. 3211 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3212 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3213 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3214 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3215 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3216 :param expDate: string "Undefined" by default or local date in future. 3217 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3218 This date is converting to UTC format for server. 3219 :return: JSON with response from broker server. 3220 """ 3221 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3222 3223 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3224 """ 3225 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3226 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3227 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3228 See also: `Order()` docstring. 3229 3230 :param lots: volume, integer count of lots >= 1. 3231 :param targetPrice: target price > 0. This is open trade price for limit order. 3232 :return: JSON with response from broker server. 3233 """ 3234 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3235 3236 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3237 """ 3238 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3239 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3240 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3241 target price value then broker opens a limit order. See also: `Order()` docstring. 3242 3243 :param lots: volume, integer count of lots >= 1. 3244 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3245 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3246 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3247 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3248 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3249 :param expDate: string "Undefined" by default or local date in future. 3250 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3251 This date is converting to UTC format for server. 3252 :return: JSON with response from broker server. 3253 """ 3254 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3255 3256 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3257 """ 3258 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3259 3260 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3261 :param allOrdersIDs: pre-received lists of all active pending orders. 3262 This avoids unnecessary downloading data from the server. 3263 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3264 """ 3265 if self.accountId is None or not self.accountId: 3266 uLogger.error("Variable `accountId` must be defined for using this method!") 3267 raise Exception("Account ID required") 3268 3269 if orderIDs: 3270 if allOrdersIDs is None or not allOrdersIDs: 3271 rawOrders = self.RequestPendingOrders() 3272 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3273 3274 if allStopOrdersIDs is None or not allStopOrdersIDs: 3275 rawStopOrders = self.RequestStopOrders() 3276 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3277 3278 for orderID in orderIDs: 3279 idInPendingOrders = orderID in allOrdersIDs 3280 idInStopOrders = orderID in allStopOrdersIDs 3281 3282 if not (idInPendingOrders or idInStopOrders): 3283 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3284 continue 3285 3286 else: 3287 if idInPendingOrders: 3288 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3289 3290 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3291 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3292 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3293 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3294 3295 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3296 if self.moreDebug: 3297 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3298 3299 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3300 3301 else: 3302 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3303 3304 elif idInStopOrders: 3305 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3306 3307 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3308 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3309 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3310 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3311 3312 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3313 if self.moreDebug: 3314 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3315 3316 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3317 3318 else: 3319 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3320 3321 else: 3322 continue 3323 3324 def CloseAllOrders(self) -> None: 3325 """ 3326 Gets a list of open pending and stop orders and cancel it all. 3327 """ 3328 rawOrders = self.RequestPendingOrders() 3329 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3330 lenOrders = len(allOrdersIDs) 3331 3332 rawStopOrders = self.RequestStopOrders() 3333 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3334 lenSOrders = len(allStopOrdersIDs) 3335 3336 if lenOrders > 0 or lenSOrders > 0: 3337 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3338 3339 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3340 3341 else: 3342 uLogger.info("Orders not found, nothing to cancel.") 3343 3344 def CloseAll(self, *args) -> None: 3345 """ 3346 Close all available (not blocked) opened trades and orders. 3347 3348 Also, you can select one or more keywords case-insensitive: 3349 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3350 3351 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3352 """ 3353 overview = self.Overview(show=False) # get all open trades info 3354 3355 if len(args) == 0: 3356 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3357 self.CloseAllOrders() # close all pending and stop orders 3358 3359 for iType in TKS_INSTRUMENTS: 3360 if iType != "Currencies": 3361 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3362 3363 else: 3364 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3365 lowerArgs = [x.lower() for x in args] 3366 3367 if "orders" in lowerArgs: 3368 self.CloseAllOrders() # close all pending and stop orders 3369 3370 for iType in TKS_INSTRUMENTS: 3371 if iType.lower() in lowerArgs and iType != "Currencies": 3372 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3373 3374 @staticmethod 3375 def ParseOrderParameters(operation, **inputParameters): 3376 """ 3377 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3378 3379 :param operation: string "Buy" or "Sell". 3380 :param inputParameters: this is dict of strings that looks like this 3381 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3382 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3383 "prices" key: one or more prices to open limit-orders 3384 Counts of values in lots and prices lists must be equals! 3385 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3386 """ 3387 # TODO: update order grid work with api v2 3388 pass 3389 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3390 # 3391 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3392 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3393 # raise Exception("Incorrect value") 3394 # 3395 # if "l" in inputParameters.keys(): 3396 # inputParameters["lots"] = inputParameters.pop("l") 3397 # 3398 # if "p" in inputParameters.keys(): 3399 # inputParameters["prices"] = inputParameters.pop("p") 3400 # 3401 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3402 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3403 # raise Exception("Incorrect value") 3404 # 3405 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3406 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3407 # 3408 # if len(lots) != len(prices): 3409 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3410 # raise Exception("Incorrect value") 3411 # 3412 # uLogger.debug("Extracted parameters for orders:") 3413 # uLogger.debug("lots = {}".format(lots)) 3414 # uLogger.debug("prices = {}".format(prices)) 3415 # 3416 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3417 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3418 # uLogger.debug("Order parameters: {}".format(result)) 3419 # 3420 # return result 3421 3422 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3423 """ 3424 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3425 3426 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3427 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3428 """ 3429 result = False 3430 msg = "Instrument not defined!" 3431 3432 if portfolio is None or not portfolio: 3433 portfolio = self.Overview(show=False) 3434 3435 if self.ticker: 3436 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3437 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3438 3439 for iType in TKS_INSTRUMENTS: 3440 for instrument in portfolio["stat"][iType]: 3441 if instrument["ticker"] == self.ticker: 3442 result = True 3443 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3444 break 3445 3446 elif self.figi: 3447 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3448 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3449 3450 for iType in TKS_INSTRUMENTS: 3451 for instrument in portfolio["stat"][iType]: 3452 if instrument["figi"] == self.figi: 3453 result = True 3454 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3455 break 3456 3457 else: 3458 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3459 3460 uLogger.debug(msg) 3461 3462 return result 3463 3464 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3465 """ 3466 Returns instrument from the user's portfolio if it presents there. 3467 Instrument must be defined by `ticker` (highly priority) or `figi`. 3468 3469 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3470 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3471 """ 3472 result = None 3473 msg = "Instrument not defined!" 3474 3475 if portfolio is None or not portfolio: 3476 portfolio = self.Overview(show=False) 3477 3478 if self.ticker: 3479 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3480 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3481 3482 for iType in TKS_INSTRUMENTS: 3483 for instrument in portfolio["stat"][iType]: 3484 if instrument["ticker"] == self.ticker: 3485 result = instrument 3486 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3487 break 3488 3489 elif self.figi: 3490 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3491 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3492 3493 for iType in TKS_INSTRUMENTS: 3494 for instrument in portfolio["stat"][iType]: 3495 if instrument["figi"] == self.figi: 3496 result = instrument 3497 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3498 break 3499 3500 else: 3501 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3502 3503 uLogger.debug(msg) 3504 3505 return result 3506 3507 def RequestLimits(self) -> dict: 3508 """ 3509 Method for obtaining the available funds for withdrawal for current `accountId`. 3510 3511 See also: 3512 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3513 - `OverviewLimits()` method 3514 3515 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3516 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3517 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3518 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3519 """ 3520 if self.accountId is None or not self.accountId: 3521 uLogger.error("Variable `accountId` must be defined for using this method!") 3522 raise Exception("Account ID required") 3523 3524 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3525 3526 self.body = str({"accountId": self.accountId}) 3527 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3528 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3529 3530 if self.moreDebug: 3531 uLogger.debug("Records about available funds for withdrawal successfully received") 3532 3533 return rawLimits 3534 3535 def OverviewLimits(self, show: bool = False) -> dict: 3536 """ 3537 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3538 3539 See also: `RequestLimits()`. 3540 3541 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3542 :return: dict with raw parsed data from server and some calculated statistics about it. 3543 """ 3544 if self.accountId is None or not self.accountId: 3545 uLogger.error("Variable `accountId` must be defined for using this method!") 3546 raise Exception("Account ID required") 3547 3548 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3549 3550 view = { 3551 "rawLimits": rawLimits, 3552 "limits": { # parsed data for every currency: 3553 "money": { # this is an array of portfolio currency positions 3554 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3555 }, 3556 "blocked": { # this is an array of blocked currency 3557 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3558 }, 3559 "blockedGuarantee": { # this is locked money under collateral for futures 3560 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3561 }, 3562 }, 3563 } 3564 3565 # --- Prepare text table with limits in human-readable format: 3566 if show: 3567 info = [ 3568 "# Withdrawal limits\n\n", 3569 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3570 "* **Account ID:** [{}]\n".format(self.accountId), 3571 ] 3572 3573 if view["limits"]["money"]: 3574 info.extend([ 3575 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3576 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3577 ]) 3578 3579 else: 3580 info.append("\nNo withdrawal limits\n") 3581 3582 for curr in view["limits"]["money"].keys(): 3583 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3584 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3585 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3586 3587 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3588 "[{}]".format(curr), 3589 "{:.2f}".format(view["limits"]["money"][curr]), 3590 "{:.2f}".format(availableMoney), 3591 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3592 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3593 ) 3594 3595 if curr == "rub": 3596 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3597 3598 else: 3599 info.append(infoStr) 3600 3601 infoText = "".join(info) 3602 3603 uLogger.info(infoText) 3604 3605 if self.withdrawalLimitsFile: 3606 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3607 fH.write(infoText) 3608 3609 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3610 3611 return view 3612 3613 def RequestAccounts(self) -> dict: 3614 """ 3615 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3616 3617 See also: 3618 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3619 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3620 - `OverviewUserInfo()` method 3621 3622 :return: dict with raw data from server that contains accounts info. Example of dict: 3623 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3624 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3625 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3626 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3627 """ 3628 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3629 3630 self.body = str({}) 3631 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3632 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3633 3634 if self.moreDebug: 3635 uLogger.debug("Records about available accounts successfully received") 3636 3637 return rawAccounts 3638 3639 def RequestUserInfo(self) -> dict: 3640 """ 3641 Method for requesting common user's information. 3642 3643 See also: 3644 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3645 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3646 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3647 - `OverviewUserInfo()` method 3648 3649 :return: dict with raw data from server that contains user's information. Example of dict: 3650 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3651 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3652 """ 3653 uLogger.debug("Requesting common user's information. Wait, please...") 3654 3655 self.body = str({}) 3656 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3657 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3658 3659 if self.moreDebug: 3660 uLogger.debug("Records about current user successfully received") 3661 3662 return rawUserInfo 3663 3664 def RequestMarginStatus(self, accountId: str = None) -> dict: 3665 """ 3666 Method for requesting margin calculation for defined account ID. 3667 3668 See also: 3669 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3670 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3671 - `OverviewUserInfo()` method 3672 3673 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3674 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3675 Example of responses: 3676 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3677 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3678 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3679 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3680 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3681 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3682 """ 3683 if accountId is None or not accountId: 3684 if self.accountId is None or not self.accountId: 3685 uLogger.error("Variable `accountId` must be defined for using this method!") 3686 raise Exception("Account ID required") 3687 3688 else: 3689 accountId = self.accountId # use `self.accountId` (main ID) by default 3690 3691 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3692 3693 self.body = str({"accountId": accountId}) 3694 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3695 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3696 3697 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3698 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3699 rawMargin = {} 3700 3701 else: 3702 if self.moreDebug: 3703 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3704 3705 return rawMargin 3706 3707 def RequestTariffLimits(self) -> dict: 3708 """ 3709 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3710 3711 See also: 3712 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3713 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3714 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3715 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3716 - `OverviewUserInfo()` method 3717 3718 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3719 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3720 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3721 """ 3722 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3723 3724 self.body = str({}) 3725 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3726 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3727 3728 if self.moreDebug: 3729 uLogger.debug("Records with limits of current tariff successfully received") 3730 3731 return rawTariffLimits 3732 3733 def RequestBondCoupons(self, iJSON: dict) -> dict: 3734 """ 3735 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3736 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3737 All dates are in UTC timezone. 3738 3739 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3740 Documentation: 3741 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3742 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3743 3744 See also: `ExtendBondsData()`. 3745 3746 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3747 If raw iJSON is not data of bond then server returns an error [400] with message: 3748 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3749 :return: dictionary with bond payment calendar. Response example 3750 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3751 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3752 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3753 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3754 """ 3755 if iJSON["figi"] is None or not iJSON["figi"]: 3756 uLogger.error("FIGI must be defined for using this method!") 3757 raise Exception("FIGI required") 3758 3759 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3760 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3761 3762 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3763 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3764 self.figi, 3765 startDate, 3766 endDate, 3767 )) 3768 3769 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3770 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3771 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3772 3773 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3774 uLogger.warning("Instrument type is not bond!") 3775 3776 else: 3777 if self.moreDebug: 3778 uLogger.debug("Records about bond payment calendar successfully received") 3779 3780 return calendar 3781 3782 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3783 """ 3784 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3785 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3786 coupon yields, current yields and some statistics etc. 3787 3788 WARNING! This is too long operation if a lot of bonds requested from broker server. 3789 3790 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3791 3792 :param instruments: list of strings with tickers or FIGIs. 3793 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3794 for further used by data scientists or stock analytics. 3795 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3796 In XLSX-file and Pandas DataFrame fields mean: 3797 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3798 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3799 """ 3800 if instruments is None or not instruments: 3801 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3802 raise Exception("Ticker or FIGI required") 3803 3804 if isinstance(instruments, str): 3805 instruments = [instruments] 3806 3807 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3808 3809 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3810 3811 iCount = len(uniqueInstruments) 3812 tooLong = iCount >= 20 3813 if tooLong: 3814 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3815 3816 bonds = None 3817 for i, self.figi in enumerate(uniqueInstruments): 3818 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3819 3820 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3821 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3822 rawBond = self.SearchByFIGI(requestPrice=True) 3823 3824 # Widen raw data with UTC current time (iData["actualDateTime"]): 3825 actualDate = datetime.now(tzutc()) 3826 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3827 3828 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3829 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3830 3831 # Replace some values with human-readable: 3832 iData["nominalCurrency"] = iData["nominal"]["currency"] 3833 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3834 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3835 iData["aciCurrency"] = iData["aciValue"]["currency"] 3836 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3837 iData["issueSize"] = int(iData["issueSize"]) 3838 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3839 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3840 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3841 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3842 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3843 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3844 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3845 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3846 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3847 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3848 3849 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3850 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3851 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3852 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3853 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3854 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3855 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3856 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3857 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3858 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3859 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3860 3861 # Widen raw data with calendar data from `rawCalendar` values: 3862 calendarData = [] 3863 if "events" in iData["rawCalendar"].keys(): 3864 for item in iData["rawCalendar"]["events"]: 3865 calendarData.append({ 3866 "couponDate": item["couponDate"], 3867 "couponNumber": int(item["couponNumber"]), 3868 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3869 "payCurrency": item["payOneBond"]["currency"], 3870 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3871 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3872 "couponStartDate": item["couponStartDate"], 3873 "couponEndDate": item["couponEndDate"], 3874 "couponPeriod": item["couponPeriod"], 3875 }) 3876 3877 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3878 if "maturityDate" not in iData.keys(): 3879 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3880 3881 # Widen raw data with Coupon Rate. 3882 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3883 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3884 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3885 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3886 3887 # Widen raw data with Yield to Maturity (YTM) on current date. 3888 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3889 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3890 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3891 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3892 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3893 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3894 3895 iData["calendar"] = calendarData # adds calendar at the end 3896 3897 # Remove not used data: 3898 iData.pop("uid") 3899 iData.pop("positionUid") 3900 iData.pop("currentPrice") 3901 iData.pop("rawCalendar") 3902 3903 colNames = list(iData.keys()) 3904 if bonds is None: 3905 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3906 3907 else: 3908 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3909 3910 else: 3911 uLogger.warning("Instrument is not a bond!") 3912 3913 processed = round(100 * (i + 1) / iCount, 1) 3914 if tooLong and processed % 5 == 0: 3915 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3916 3917 else: 3918 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3919 3920 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3921 3922 # Saving bonds from Pandas DataFrame to XLSX sheet: 3923 if xlsx and self.bondsXLSXFile: 3924 with pd.ExcelWriter( 3925 path=self.bondsXLSXFile, 3926 date_format=TKS_DATE_FORMAT, 3927 datetime_format=TKS_DATE_TIME_FORMAT, 3928 mode="w", 3929 ) as writer: 3930 bonds.to_excel( 3931 writer, 3932 sheet_name="Extended bonds data", 3933 index=True, 3934 encoding="UTF-8", 3935 freeze_panes=(1, 1), 3936 ) # saving as XLSX-file with freeze first row and column as headers 3937 3938 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3939 3940 return bonds 3941 3942 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3943 """ 3944 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3945 3946 WARNING! This is too long operation if a lot of bonds requested from broker server. 3947 3948 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3949 3950 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3951 extended information about bonds: main info, current prices, bond payment calendar, 3952 coupon yields, current yields and some statistics etc. 3953 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3954 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3955 for further used by data scientists or stock analytics. 3956 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3957 """ 3958 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3959 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3960 3961 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3962 3963 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3964 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3965 calendar = None 3966 for bond in extBonds.iterrows(): 3967 for item in bond[1]["calendar"]: 3968 cData = { 3969 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3970 "couponDate": item["couponDate"], 3971 "figi": bond[1]["figi"], 3972 "ticker": bond[1]["ticker"], 3973 "name": bond[1]["name"], 3974 "couponNumber": item["couponNumber"], 3975 "payOneBond": item["payOneBond"], 3976 "payCurrency": item["payCurrency"], 3977 "couponType": item["couponType"], 3978 "couponPeriod": item["couponPeriod"], 3979 "fixDate": item["fixDate"], 3980 "couponStartDate": item["couponStartDate"], 3981 "couponEndDate": item["couponEndDate"], 3982 } 3983 3984 if calendar is None: 3985 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3986 3987 else: 3988 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 3989 3990 if calendar is not None: 3991 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 3992 3993 # Saving calendar from Pandas DataFrame to XLSX sheet: 3994 if xlsx: 3995 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 3996 3997 with pd.ExcelWriter( 3998 path=xlsxCalendarFile, 3999 date_format=TKS_DATE_FORMAT, 4000 datetime_format=TKS_DATE_TIME_FORMAT, 4001 mode="w", 4002 ) as writer: 4003 humanReadable = calendar.copy(deep=True) 4004 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4005 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4006 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4007 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4008 humanReadable.columns = colNames # human-readable column names 4009 4010 humanReadable.to_excel( 4011 writer, 4012 sheet_name="Bond payments calendar", 4013 index=False, 4014 encoding="UTF-8", 4015 freeze_panes=(1, 2), 4016 ) # saving as XLSX-file with freeze first row and column as headers 4017 4018 del humanReadable # release df in memory 4019 4020 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4021 4022 return calendar 4023 4024 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4025 """ 4026 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4027 Also, creates Markdown file with calendar data, `calendar.md` by default. 4028 4029 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4030 4031 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4032 extended information about bonds: main info, current prices, bond payment calendar, 4033 coupon yields, current yields and some statistics etc. 4034 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4035 :param show: if `True` then also printing bonds payment calendar to the console, 4036 otherwise save to file `calendarFile` only. `False` by default. 4037 :return: multilines text in Markdown format with bonds payment calendar as a table. 4038 """ 4039 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4040 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4041 4042 infoText = "# Bond payments calendar\n\n" 4043 4044 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4045 4046 if not (calendar is None or calendar.empty): 4047 splitLine = "| | | | | | | | | |\n" 4048 4049 info = [ 4050 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4051 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4052 ] 4053 4054 newMonth = False 4055 notOneBond = calendar["figi"].nunique() > 1 4056 for i, bond in enumerate(calendar.iterrows()): 4057 if newMonth and notOneBond: 4058 info.append(splitLine) 4059 4060 info.append( 4061 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4062 " √" if bond[1]["paid"] else " —", 4063 bond[1]["couponDate"].split("T")[0], 4064 bond[1]["figi"], 4065 bond[1]["ticker"], 4066 bond[1]["couponNumber"], 4067 "{} {}".format( 4068 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4069 bond[1]["payCurrency"], 4070 ), 4071 bond[1]["couponType"], 4072 bond[1]["couponPeriod"], 4073 bond[1]["fixDate"].split("T")[0], 4074 ) 4075 ) 4076 4077 if i < len(calendar.values) - 1: 4078 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4079 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4080 newMonth = False if curDate.month == nextDate.month else True 4081 4082 else: 4083 newMonth = False 4084 4085 infoText += "".join(info) 4086 4087 if show: 4088 uLogger.info("{}".format(infoText)) 4089 4090 if self.calendarFile is not None: 4091 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4092 fH.write(infoText) 4093 4094 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4095 4096 else: 4097 infoText += "No data\n" 4098 4099 return infoText 4100 4101 def OverviewAccounts(self, show: bool = False) -> dict: 4102 """ 4103 Method for parsing and show simple table with all available user accounts. 4104 4105 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4106 4107 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4108 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4109 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4110 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4111 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4112 "closed": "—", "access": "Full access" }, ...}}` 4113 """ 4114 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4115 4116 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4117 accounts = { 4118 item["id"]: { 4119 "type": TKS_ACCOUNT_TYPES[item["type"]], 4120 "name": item["name"], 4121 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4122 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4123 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4124 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4125 } for item in rawAccounts["accounts"] 4126 } 4127 4128 # Raw and parsed data with some fields replaced in "stat" section: 4129 view = { 4130 "rawAccounts": rawAccounts, 4131 "stat": accounts, 4132 } 4133 4134 # --- Prepare simple text table with only accounts data in human-readable format: 4135 if show: 4136 info = [ 4137 "# User accounts\n\n", 4138 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4139 "| Account ID | Type | Status | Name |\n", 4140 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4141 ] 4142 4143 for account in view["stat"].keys(): 4144 info.extend([ 4145 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4146 account, 4147 view["stat"][account]["type"], 4148 view["stat"][account]["status"], 4149 view["stat"][account]["name"], 4150 ) 4151 ]) 4152 4153 infoText = "".join(info) 4154 4155 uLogger.info(infoText) 4156 4157 if self.userAccountsFile: 4158 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4159 fH.write(infoText) 4160 4161 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4162 4163 return view 4164 4165 def OverviewUserInfo(self, show: bool = False) -> dict: 4166 """ 4167 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4168 4169 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4170 4171 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4172 :return: dict with raw parsed data from server and some calculated statistics about it. 4173 """ 4174 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4175 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4176 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4177 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4178 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4179 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4180 4181 # This is dict with parsed common user data: 4182 userInfo = { 4183 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4184 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4185 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4186 "tariff": rawUserInfo["tariff"], 4187 } 4188 4189 # This is an array of dict with parsed margin statuses for every account IDs: 4190 margins = {} 4191 for accountId in accounts.keys(): 4192 if rawMargins[accountId]: 4193 margins[accountId] = { 4194 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4195 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4196 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4197 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4198 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4199 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4200 } 4201 4202 else: 4203 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4204 4205 unary = {} # unary-connection limits 4206 for item in rawTariffLimits["unaryLimits"]: 4207 if item["limitPerMinute"] in unary.keys(): 4208 unary[item["limitPerMinute"]].extend(item["methods"]) 4209 4210 else: 4211 unary[item["limitPerMinute"]] = item["methods"] 4212 4213 stream = {} # stream-connection limits 4214 for item in rawTariffLimits["streamLimits"]: 4215 if item["limit"] in stream.keys(): 4216 stream[item["limit"]].extend(item["streams"]) 4217 4218 else: 4219 stream[item["limit"]] = item["streams"] 4220 4221 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4222 limits = { 4223 "unary": unary, 4224 "stream": stream, 4225 } 4226 4227 # Raw and parsed data as an output result: 4228 view = { 4229 "rawUserInfo": rawUserInfo, 4230 "rawAccounts": rawAccounts, 4231 "rawMargins": rawMargins, 4232 "rawTariffLimits": rawTariffLimits, 4233 "stat": { 4234 "userInfo": userInfo, 4235 "accounts": accounts, 4236 "margins": margins, 4237 "limits": limits, 4238 }, 4239 } 4240 4241 # --- Prepare text table with user information in human-readable format: 4242 if show: 4243 info = [ 4244 "# Full user information\n\n", 4245 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4246 "## Common information\n\n", 4247 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4248 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4249 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4250 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4251 "\n## User accounts\n\n", 4252 ] 4253 4254 for account in view["stat"]["accounts"].keys(): 4255 info.extend([ 4256 "### ID: [{}]\n\n".format(account), 4257 "| Parameters | Values |\n", 4258 "|----------------------|--------------------------------------------------------------|\n", 4259 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4260 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4261 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4262 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4263 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4264 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4265 ]) 4266 4267 if margins[account]: 4268 info.extend([ 4269 "| Margin status: | Enabled |\n", 4270 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4271 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4272 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4273 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4274 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4275 ]) 4276 4277 else: 4278 info.append("| Margin status: | Disabled |\n\n") 4279 4280 info.extend([ 4281 "\n## Current user tariff limits\n", 4282 "\nSee also:\n", 4283 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4284 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4285 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4286 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4287 "\n### Unary limits\n", 4288 ]) 4289 4290 if unary: 4291 for key, values in sorted(unary.items()): 4292 info.append("\n* Max requests per minute: {}\n".format(key)) 4293 4294 for value in values: 4295 info.append(" - {}\n".format(value)) 4296 4297 else: 4298 info.append("\nNot available\n") 4299 4300 info.append("\n### Stream limits\n") 4301 4302 if stream: 4303 for key, values in sorted(stream.items()): 4304 info.append("\n* Max stream connections: {}\n".format(key)) 4305 4306 for value in values: 4307 info.append(" - {}\n".format(value)) 4308 4309 else: 4310 info.append("\nNot available\n") 4311 4312 infoText = "".join(info) 4313 4314 uLogger.info(infoText) 4315 4316 if self.userInfoFile: 4317 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4318 fH.write(infoText) 4319 4320 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4321 4322 return view 4323 4324 4325class Args: 4326 """ 4327 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4328 """ 4329 def __init__(self, **kwargs): 4330 self.__dict__.update(kwargs) 4331 4332 def __getattr__(self, item): 4333 return None 4334 4335 4336def ParseArgs(): 4337 """This function get and parse command line keys.""" 4338 parser = ArgumentParser() # command-line string parser 4339 4340 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4341 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4342 4343 # --- options: 4344 4345 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4346 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4347 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4348 4349 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4350 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4351 4352 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4353 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4354 4355 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4356 4357 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4358 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4359 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4360 4361 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4362 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4363 4364 # --- commands: 4365 4366 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4367 4368 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4369 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4370 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4371 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4372 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4373 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4374 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4375 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4376 4377 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4378 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4379 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4380 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4381 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4382 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4383 4384 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4385 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4386 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4387 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4388 4389 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4390 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4391 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4392 4393 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4394 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4395 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4396 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4397 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4398 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4399 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4400 4401 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4402 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4403 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4404 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4405 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4406 4407 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4408 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4409 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4410 4411 cmdArgs = parser.parse_args() 4412 return cmdArgs 4413 4414 4415def Main(**kwargs): 4416 """ 4417 Main function for work with TKSBrokerAPI in the console. 4418 4419 See examples: 4420 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4421 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4422 """ 4423 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4424 4425 if args.debug_level: 4426 uLogger.level = 10 # always debug level by default 4427 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4428 4429 exitCode = 0 4430 start = datetime.now(tzutc()) 4431 uLogger.debug("=-" * 50) 4432 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4433 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4434 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4435 )) 4436 4437 # trying to calculate full current version: 4438 buildVersion = __version__ 4439 try: 4440 v = version("tksbrokerapi") 4441 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4442 4443 except Exception: 4444 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4445 4446 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4447 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4448 4449 try: 4450 if args.version: 4451 print("TKSBrokerAPI {}".format(buildVersion)) 4452 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4453 4454 else: 4455 # Init class for trading with Tinkoff Broker: 4456 trader = TinkoffBrokerServer( 4457 token=args.token, 4458 accountId=args.account_id, 4459 useCache=not args.no_cache, 4460 ) 4461 4462 # --- set some options: 4463 4464 if args.more: 4465 trader.moreDebug = True 4466 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4467 4468 if args.ticker: 4469 ticker = args.ticker.upper() # Tickers may be upper case only 4470 4471 if ticker in trader.aliasesKeys: 4472 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4473 4474 else: 4475 trader.ticker = ticker 4476 4477 if args.figi: 4478 trader.figi = args.figi.upper() # FIGIs may be upper case only 4479 4480 if args.depth is not None: 4481 trader.depth = args.depth 4482 4483 # --- do one command: 4484 4485 if args.list: 4486 if args.output is not None: 4487 trader.instrumentsFile = args.output 4488 4489 trader.ShowInstrumentsInfo(show=True) 4490 4491 elif args.list_xlsx: 4492 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4493 4494 elif args.bonds_xlsx is not None: 4495 if args.output is not None: 4496 trader.bondsXLSXFile = args.output 4497 4498 if len(args.bonds_xlsx) == 0: 4499 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4500 4501 else: 4502 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4503 4504 elif args.search: 4505 if args.output is not None: 4506 trader.searchResultsFile = args.output 4507 4508 trader.SearchInstruments(pattern=args.search[0], show=True) 4509 4510 elif args.info: 4511 if not (args.ticker or args.figi): 4512 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4513 raise Exception("Ticker or FIGI required") 4514 4515 if args.output is not None: 4516 trader.infoFile = args.output 4517 4518 if args.ticker: 4519 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4520 4521 else: 4522 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4523 4524 elif args.calendar is not None: 4525 if args.output is not None: 4526 trader.calendarFile = args.output 4527 4528 if len(args.calendar) == 0: 4529 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4530 4531 else: 4532 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4533 4534 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4535 4536 elif args.price: 4537 if not (args.ticker or args.figi): 4538 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4539 raise Exception("Ticker or FIGI required") 4540 4541 trader.GetCurrentPrices(show=True) 4542 4543 elif args.prices is not None: 4544 if args.output is not None: 4545 trader.pricesFile = args.output 4546 4547 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4548 4549 elif args.overview: 4550 if args.output is not None: 4551 trader.overviewFile = args.output 4552 4553 trader.Overview(show=True, details="full") 4554 4555 elif args.overview_digest: 4556 if args.output is not None: 4557 trader.overviewDigestFile = args.output 4558 4559 trader.Overview(show=True, details="digest") 4560 4561 elif args.overview_positions: 4562 if args.output is not None: 4563 trader.overviewPositionsFile = args.output 4564 4565 trader.Overview(show=True, details="positions") 4566 4567 elif args.overview_orders: 4568 if args.output is not None: 4569 trader.overviewOrdersFile = args.output 4570 4571 trader.Overview(show=True, details="orders") 4572 4573 elif args.overview_analytics: 4574 if args.output is not None: 4575 trader.overviewAnalyticsFile = args.output 4576 4577 trader.Overview(show=True, details="analytics") 4578 4579 elif args.overview_calendar: 4580 if args.output is not None: 4581 trader.overviewAnalyticsFile = args.output 4582 4583 trader.Overview(show=True, details="calendar") 4584 4585 elif args.deals is not None: 4586 if args.output is not None: 4587 trader.reportFile = args.output 4588 4589 if 0 <= len(args.deals) < 3: 4590 trader.Deals( 4591 start=args.deals[0] if len(args.deals) >= 1 else None, 4592 end=args.deals[1] if len(args.deals) == 2 else None, 4593 show=True, # Always show deals report in console 4594 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4595 ) 4596 4597 else: 4598 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4599 raise Exception("Incorrect value") 4600 4601 elif args.history is not None: 4602 if args.output is not None: 4603 trader.historyFile = args.output 4604 4605 if 0 <= len(args.history) < 3: 4606 dataReceived = trader.History( 4607 start=args.history[0] if len(args.history) >= 1 else None, 4608 end=args.history[1] if len(args.history) == 2 else None, 4609 interval="hour" if args.interval is None or not args.interval else args.interval, 4610 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4611 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4612 show=True, # shows all downloaded candles in console 4613 ) 4614 4615 if args.render_chart is not None and dataReceived is not None: 4616 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4617 4618 trader.ShowHistoryChart( 4619 candles=dataReceived, 4620 interact=iChart, 4621 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4622 ) 4623 4624 else: 4625 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4626 raise Exception("Incorrect value") 4627 4628 elif args.load_history is not None: 4629 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4630 4631 if args.render_chart is not None and histData is not None: 4632 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4633 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4634 4635 trader.ShowHistoryChart( 4636 candles=histData, 4637 interact=iChart, 4638 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4639 ) 4640 4641 elif args.trade is not None: 4642 if 1 <= len(args.trade) <= 5: 4643 trader.Trade( 4644 operation=args.trade[0], 4645 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4646 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4647 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4648 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4649 ) 4650 4651 else: 4652 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4653 4654 elif args.buy is not None: 4655 if 0 <= len(args.buy) <= 4: 4656 trader.Buy( 4657 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4658 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4659 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4660 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4661 ) 4662 4663 else: 4664 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4665 4666 elif args.sell is not None: 4667 if 0 <= len(args.sell) <= 4: 4668 trader.Sell( 4669 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4670 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4671 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4672 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4673 ) 4674 4675 else: 4676 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4677 4678 elif args.order: 4679 if 4 <= len(args.order) <= 7: 4680 trader.Order( 4681 operation=args.order[0], 4682 orderType=args.order[1], 4683 lots=int(args.order[2]), 4684 targetPrice=float(args.order[3]), 4685 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4686 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4687 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4688 ) 4689 4690 else: 4691 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4692 4693 elif args.buy_limit: 4694 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4695 4696 elif args.sell_limit: 4697 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4698 4699 elif args.buy_stop: 4700 if 2 <= len(args.buy_stop) <= 7: 4701 trader.BuyStop( 4702 lots=int(args.buy_stop[0]), 4703 targetPrice=float(args.buy_stop[1]), 4704 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4705 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4706 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4707 ) 4708 4709 else: 4710 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4711 4712 elif args.sell_stop: 4713 if 2 <= len(args.sell_stop) <= 7: 4714 trader.SellStop( 4715 lots=int(args.sell_stop[0]), 4716 targetPrice=float(args.sell_stop[1]), 4717 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4718 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4719 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4720 ) 4721 4722 else: 4723 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4724 4725 # elif args.buy_order_grid is not None: 4726 # # update order grid work with api v2 4727 # if len(args.buy_order_grid) == 2: 4728 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4729 # 4730 # for order in orderParams: 4731 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4732 # 4733 # else: 4734 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4735 # 4736 # elif args.sell_order_grid is not None: 4737 # # update order grid work with api v2 4738 # if len(args.sell_order_grid) >= 2: 4739 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4740 # 4741 # for order in orderParams: 4742 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4743 # 4744 # else: 4745 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4746 4747 elif args.close_order is not None: 4748 trader.CloseOrders(args.close_order) # close only one order 4749 4750 elif args.close_orders is not None: 4751 trader.CloseOrders(args.close_orders) # close list of orders 4752 4753 elif args.close_trade: 4754 if not (args.ticker or args.figi): 4755 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4756 raise Exception("Ticker or FIGI required") 4757 4758 if args.ticker: 4759 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4760 4761 else: 4762 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4763 4764 elif args.close_trades is not None: 4765 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4766 4767 elif args.close_all is not None: 4768 trader.CloseAll(*args.close_all) 4769 4770 elif args.limits: 4771 if args.output is not None: 4772 trader.withdrawalLimitsFile = args.output 4773 4774 trader.OverviewLimits(show=True) 4775 4776 elif args.user_info: 4777 if args.output is not None: 4778 trader.userInfoFile = args.output 4779 4780 trader.OverviewUserInfo(show=True) 4781 4782 elif args.account: 4783 if args.output is not None: 4784 trader.userAccountsFile = args.output 4785 4786 trader.OverviewAccounts(show=True) 4787 4788 else: 4789 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4790 raise Exception("There is no command to execute") 4791 4792 except Exception: 4793 trace = tb.format_exc() 4794 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4795 if e in trace: 4796 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4797 break 4798 4799 uLogger.debug(trace) 4800 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4801 exitCode = 255 # an error occurred, must be open a ticket for this issue 4802 4803 finally: 4804 finish = datetime.now(tzutc()) 4805 4806 if exitCode == 0: 4807 if args.more: 4808 uLogger.debug("All operations were finished success (summary code is 0).") 4809 4810 else: 4811 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4812 os.path.abspath(uLog.defaultLogFile), exitCode, 4813 )) 4814 4815 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4816 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4817 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4818 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4819 )) 4820 uLogger.debug("=-" * 50) 4821 4822 if not kwargs: 4823 sys.exit(exitCode) 4824 4825 else: 4826 return exitCode 4827 4828 4829if __name__ == "__main__": 4830 Main()
76class TinkoffBrokerServer: 77 """ 78 This class implements methods to work with Tinkoff broker server. 79 80 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 81 82 About `token`: https://tinkoff.github.io/investAPI/token/ 83 """ 84 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 85 """ 86 Main class init. 87 88 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 89 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 90 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 91 :param useCache: use default cache file with raw data to use instead of `iList`. 92 True by default. Cache is auto-update if new day has come. 93 If you don't want to use cache and always updates raw data then set `useCache=False`. 94 :param defaultCache: path to default cache file. `dump.json` by default. 95 """ 96 if token is None or not token: 97 try: 98 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 99 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 100 101 except KeyError: 102 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 103 raise Exception("Token required") 104 105 else: 106 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 107 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 108 109 if accountId is None or not accountId: 110 try: 111 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 112 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 113 114 except KeyError: 115 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 116 117 else: 118 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 119 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 120 121 self.version = __version__ # duplicate here used TKSBrokerAPI main version 122 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 123 124 Latest version: https://pypi.org/project/tksbrokerapi/ 125 """ 126 127 self.aliases = TKS_TICKER_ALIASES 128 """Some aliases instead official tickers. 129 130 See also: `TKSEnums.TKS_TICKER_ALIASES` 131 """ 132 133 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 134 135 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 136 137 self.ticker = "" 138 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 139 140 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 141 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 142 143 See also: `SearchByTicker()`, `SearchInstruments()`. 144 """ 145 146 self.figi = "" 147 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 148 149 See also: `SearchByFIGI()`, `SearchInstruments()`. 150 """ 151 152 self.depth = 1 153 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 154 155 See also: `GetCurrentPrices()`. 156 """ 157 158 self.server = r"https://invest-public-api.tinkoff.ru/rest" 159 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 160 161 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 162 """ 163 164 uLogger.debug("Broker API server: {}".format(self.server)) 165 166 self.timeout = 15 167 """Server operations timeout in seconds. Default: `15`. 168 169 See also: `SendAPIRequest()`. 170 """ 171 172 self.headers = { 173 "Content-Type": "application/json", 174 "accept": "application/json", 175 "Authorization": "Bearer {}".format(self.token), 176 "x-app-name": "Tim55667757.TKSBrokerAPI", 177 } 178 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.body = None 184 """Request body which send to broker server. Default: `None`. 185 186 See also: `SendAPIRequest()`. 187 """ 188 189 self.moreDebug = False 190 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 191 192 self.historyFile = None 193 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 194 195 See also: `History()`. 196 """ 197 198 self.htmlHistoryFile = "index.html" 199 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 200 201 See also: `ShowHistoryChart()`. 202 """ 203 204 self.instrumentsFile = "instruments.md" 205 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 206 207 See also: `ShowInstrumentsInfo()`. 208 """ 209 210 self.searchResultsFile = "search-results.md" 211 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 212 213 See also: `SearchInstruments()`. 214 """ 215 216 self.pricesFile = "prices.md" 217 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 218 219 See also: `GetListOfPrices()`. 220 """ 221 222 self.infoFile = "info.md" 223 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 224 225 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 226 """ 227 228 self.bondsXLSXFile = "ext-bonds.xlsx" 229 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 230 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 231 232 See also: `ExtendBondsData()`. 233 """ 234 235 self.calendarFile = "calendar.md" 236 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 237 238 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 239 240 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 241 """ 242 243 self.overviewFile = "overview.md" 244 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 245 246 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 247 """ 248 249 self.overviewDigestFile = "overview-digest.md" 250 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 251 252 See also: `Overview()` with parameter `details="digest"`. 253 """ 254 255 self.overviewPositionsFile = "overview-positions.md" 256 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 257 258 See also: `Overview()` with parameter `details="positions"`. 259 """ 260 261 self.overviewOrdersFile = "overview-orders.md" 262 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 263 264 See also: `Overview()` with parameter `details="orders"`. 265 """ 266 267 self.overviewAnalyticsFile = "overview-analytics.md" 268 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 269 270 See also: `Overview()` with parameter `details="analytics"`. 271 """ 272 273 self.overviewBondsCalendarFile = "overview-calendar.md" 274 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 275 276 See also: `Overview()` with parameter `details="calendar"`. 277 """ 278 279 self.reportFile = "deals.md" 280 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 281 282 See also: `Deals()`. 283 """ 284 285 self.withdrawalLimitsFile = "limits.md" 286 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 287 288 See also: `OverviewLimits()` and `RequestLimits()`. 289 """ 290 291 self.userInfoFile = "user-info.md" 292 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 293 294 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 295 """ 296 297 self.userAccountsFile = "accounts.md" 298 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 299 300 See also: `OverviewAccounts()`, `RequestAccounts()`. 301 """ 302 303 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 304 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 305 306 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 307 308 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 309 """ 310 311 self.iList = None # init iList for raw instruments data 312 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 313 314 See also: `Listing()`, `DumpInstruments()`. 315 """ 316 317 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 318 if useCache: 319 if os.path.exists(self.iListDumpFile): 320 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 321 curTime = datetime.now(tzutc()) 322 323 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 324 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 325 326 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 327 328 else: 329 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 330 331 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 332 os.path.abspath(self.iListDumpFile), 333 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 334 )) 335 336 else: 337 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 338 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 339 340 else: 341 self.iList = self.Listing() # request new raw instruments data from broker server 342 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 343 344 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 345 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 346 347 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 348 """ 349 350 def _ParseJSON(self, rawData="{}") -> dict: 351 """ 352 Parse JSON from response string. 353 354 :param rawData: this is a string with JSON-formatted text. 355 :return: JSON (dictionary), parsed from server response string. 356 """ 357 responseJSON = json.loads(rawData) if rawData else {} 358 359 if self.moreDebug: 360 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 361 362 return responseJSON 363 364 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 365 """ 366 Send GET or POST request to broker server and receive JSON object. 367 368 self.header: must be defining with dictionary of headers. 369 self.body: if define then used as request body. None by default. 370 self.timeout: global request timeout, 15 seconds by default. 371 :param url: url with REST request. 372 :param reqType: send "GET" or "POST" request. "GET" by default. 373 :param retry: how many times retry after first request if an 5xx server errors occurred. 374 :param pause: sleep time in seconds between retries. 375 :return: response JSON (dictionary) from broker. 376 """ 377 if reqType not in ("GET", "POST"): 378 uLogger.error("You can define request type: 'GET' or 'POST'!") 379 raise Exception("Incorrect value") 380 381 if self.moreDebug: 382 uLogger.debug("Request parameters:") 383 uLogger.debug(" - REST API URL: {}".format(url)) 384 uLogger.debug(" - request type: {}".format(reqType)) 385 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 386 uLogger.debug(" - body:\n{}".format(self.body)) 387 388 # fast hack to avoid all operations with some tickers/FIGI 389 responseJSON = {} 390 oK = True 391 for item in self.exclude: 392 if item in url: 393 if self.moreDebug: 394 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 395 396 oK = False 397 break 398 399 if oK: 400 counter = 0 401 response = None 402 errMsg = "" 403 404 while not response and counter <= retry: 405 if reqType == "GET": 406 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 407 408 if reqType == "POST": 409 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 410 411 if self.moreDebug: 412 uLogger.debug("Response:") 413 uLogger.debug(" - status code: {}".format(response.status_code)) 414 uLogger.debug(" - reason: {}".format(response.reason)) 415 uLogger.debug(" - body length: {}".format(len(response.text))) 416 uLogger.debug(" - headers:\n{}".format(response.headers)) 417 418 # Server returns some headers: 419 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 420 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 421 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 422 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 423 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 424 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 425 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 426 sleep(rateLimitWait) 427 428 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 429 if 400 <= response.status_code < 500: 430 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 431 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 432 counter = retry + 1 433 434 if 500 <= response.status_code < 600: 435 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 436 uLogger.debug(" - not oK, {}".format(errMsg)) 437 counter += 1 438 439 if counter <= retry: 440 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 441 sleep(pause) 442 443 responseJSON = self._ParseJSON(rawData=response.text) 444 445 if errMsg: 446 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 447 uLogger.error(" - not oK, {}".format(errMsg)) 448 449 return responseJSON 450 451 def _IUpdater(self, iType: str) -> tuple: 452 """ 453 Request instrument by type from server. See available API methods for instruments: 454 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 455 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 456 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 457 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 458 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 459 460 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 461 :return: tuple with iType name and list of available instruments of current type for defined user token. 462 """ 463 result = [] 464 465 if iType in TKS_INSTRUMENTS: 466 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 467 468 # all instruments have the same body in API v2 requests: 469 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 470 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 471 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 472 473 return iType, result 474 475 def _IWrapper(self, kwargs): 476 """ 477 Wrapper runs instrument's update method `_IUpdater()`. 478 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 479 """ 480 return self._IUpdater(**kwargs) 481 482 def Listing(self) -> dict: 483 """ 484 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 485 486 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 487 """ 488 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 489 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 490 491 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 492 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 493 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 494 495 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 496 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 497 poolUpdater.close() 498 499 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 500 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 501 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 502 503 # calculate minimum price increment (step) for all instruments and set up instrument's type: 504 for iType in iList.keys(): 505 for ticker in iList[iType]: 506 iList[iType][ticker]["type"] = iType 507 508 if "minPriceIncrement" in iList[iType][ticker].keys(): 509 iList[iType][ticker]["step"] = NanoToFloat( 510 iList[iType][ticker]["minPriceIncrement"]["units"], 511 iList[iType][ticker]["minPriceIncrement"]["nano"], 512 ) 513 514 else: 515 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 516 517 return iList 518 519 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 520 """ 521 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 522 523 See also: `DumpInstruments()`, `Listing()`. 524 525 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 526 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 527 """ 528 if self.iListDumpFile is None or not self.iListDumpFile: 529 uLogger.error("Output name of dump file must be defined!") 530 raise Exception("Filename required") 531 532 if not self.iList or forceUpdate: 533 self.iList = self.Listing() 534 535 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 536 537 # Save as XLSX with separated sheets for every type of instruments: 538 with pd.ExcelWriter( 539 path=xlsxDumpFile, 540 date_format=TKS_DATE_FORMAT, 541 datetime_format=TKS_DATE_TIME_FORMAT, 542 mode="w", 543 ) as writer: 544 for iType in TKS_INSTRUMENTS: 545 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 546 df = df[sorted(df)] # sorted by column names 547 df = df.applymap( 548 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 549 na_action="ignore", 550 ) # converting numbers from nano-type to float in every cell 551 df.to_excel( 552 writer, 553 sheet_name=iType, 554 encoding="UTF-8", 555 freeze_panes=(1, 1), 556 ) # saving as XLSX-file with freeze first row and column as headers 557 558 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 559 560 def DumpInstruments(self, forceUpdate: bool = True) -> str: 561 """ 562 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 563 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 564 565 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 566 567 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 568 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 569 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 570 """ 571 if self.iListDumpFile is None or not self.iListDumpFile: 572 uLogger.error("Output name of dump file must be defined!") 573 raise Exception("Filename required") 574 575 if not self.iList or forceUpdate: 576 self.iList = self.Listing() 577 578 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 579 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 580 fH.write(jsonDump) 581 582 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 583 584 return jsonDump 585 586 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 587 """ 588 Show information about one instrument defined by json data and prints it in Markdown format. 589 590 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 591 592 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 593 :param show: if `True` then also printing information about instrument and its current price. 594 :return: multilines text in Markdown format with information about one instrument. 595 """ 596 splitLine = "| | |\n" 597 infoText = "" 598 599 if iJSON is not None and iJSON and isinstance(iJSON, dict): 600 info = [ 601 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 602 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 603 "| Parameters | Values |\n", 604 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 605 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 606 "| Full name: | {:<54} |\n".format(iJSON["name"]), 607 ] 608 609 if "sector" in iJSON.keys() and iJSON["sector"]: 610 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 611 612 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 613 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 614 615 info.extend([ 616 splitLine, 617 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 618 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 619 ]) 620 621 if "isin" in iJSON.keys() and iJSON["isin"]: 622 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 623 624 if "classCode" in iJSON.keys(): 625 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 626 627 info.extend([ 628 splitLine, 629 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 630 splitLine, 631 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 632 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 633 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 634 ]) 635 636 if iJSON["figi"]: 637 self.figi = iJSON["figi"] 638 iJSON = iJSON | self.RequestTradingStatus() 639 640 info.extend([ 641 splitLine, 642 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 643 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 644 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 645 ]) 646 647 info.append(splitLine) 648 649 if "type" in iJSON.keys() and iJSON["type"]: 650 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 651 652 if "shareType" in iJSON.keys() and iJSON["shareType"]: 653 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 654 655 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 656 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 657 658 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 659 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 660 661 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 662 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 663 664 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 665 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 666 667 if "focusType" in iJSON.keys() and iJSON["focusType"]: 668 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 669 670 if "assetType" in iJSON.keys() and iJSON["assetType"]: 671 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 672 673 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 674 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 675 676 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 677 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 678 679 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 680 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 681 682 if "currency" in iJSON.keys(): 683 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 684 685 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 686 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 687 688 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 689 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 690 691 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 692 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 693 694 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 695 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 696 697 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 698 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 699 700 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 701 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 702 703 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 704 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 705 706 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 707 info.append("| Perpetual bond: | Yes |\n") 708 709 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 710 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 711 712 iExt = None 713 if iJSON["type"] == "Bonds": 714 info.extend([ 715 splitLine, 716 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 717 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 718 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 719 iJSON["nominal"]["currency"], 720 )), 721 ]) 722 723 if "floatingCouponFlag" in iJSON.keys(): 724 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 725 726 if "amortizationFlag" in iJSON.keys(): 727 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 728 729 info.append(splitLine) 730 731 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 732 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 733 734 if iJSON["figi"]: 735 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 736 737 info.extend([ 738 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 739 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 740 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 741 ]) 742 743 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 744 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 745 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 746 iJSON["aciValue"]["currency"] 747 ))) 748 749 if "currentPrice" in iJSON.keys(): 750 info.append(splitLine) 751 752 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 753 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 754 755 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 756 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 757 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 758 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 759 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 760 761 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 762 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 763 764 info.extend([ 765 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 766 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 767 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 768 )), 769 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 770 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 771 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 772 )), 773 "| Changes between last deal price and last close | {:<54} |\n".format( 774 "{:.2f}%{}".format( 775 iJSON["currentPrice"]["changes"], 776 " ({}{:.2f} {})".format( 777 "+" if bondChangesDelta > 0 else "", 778 bondChangesDelta, 779 aciCurrency 780 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 781 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 782 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 783 currency 784 ), 785 ) 786 ), 787 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 788 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 789 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 790 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 791 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 792 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 793 )), 794 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 795 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 796 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 797 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 798 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 799 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 800 )), 801 ]) 802 803 if "lot" in iJSON.keys(): 804 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 805 806 if "step" in iJSON.keys() and iJSON["step"] != 0: 807 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 808 809 # Add bond payment calendar: 810 if iJSON["type"] == "Bonds": 811 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 812 info.extend(["\n", strCalendar]) 813 814 infoText += "".join(info) 815 816 if show: 817 uLogger.info("{}".format(infoText)) 818 819 else: 820 uLogger.debug("{}".format(infoText)) 821 822 if self.infoFile is not None: 823 with open(self.infoFile, "w", encoding="UTF-8") as fH: 824 fH.write(infoText) 825 826 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 827 828 return infoText 829 830 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 831 """ 832 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 833 834 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 835 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 836 :return: JSON formatted data with information about instrument. 837 """ 838 tickerJSON = {} 839 if self.moreDebug: 840 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 841 842 if not self.ticker: 843 uLogger.warning("self.ticker variable is not be empty!") 844 845 else: 846 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 847 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 848 raise Exception("Instrument not allowed") 849 850 if not self.iList: 851 self.iList = self.Listing() 852 853 if self.ticker in self.iList["Shares"].keys(): 854 tickerJSON = self.iList["Shares"][self.ticker] 855 if self.moreDebug: 856 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 857 858 elif self.ticker in self.iList["Currencies"].keys(): 859 tickerJSON = self.iList["Currencies"][self.ticker] 860 if self.moreDebug: 861 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 862 863 elif self.ticker in self.iList["Bonds"].keys(): 864 tickerJSON = self.iList["Bonds"][self.ticker] 865 if self.moreDebug: 866 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 867 868 elif self.ticker in self.iList["Etfs"].keys(): 869 tickerJSON = self.iList["Etfs"][self.ticker] 870 if self.moreDebug: 871 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 872 873 elif self.ticker in self.iList["Futures"].keys(): 874 tickerJSON = self.iList["Futures"][self.ticker] 875 if self.moreDebug: 876 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 877 878 if tickerJSON: 879 self.figi = tickerJSON["figi"] 880 881 if requestPrice: 882 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 883 884 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 885 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 886 887 else: 888 tickerJSON["currentPrice"]["changes"] = 0 889 890 if show: 891 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 892 893 else: 894 if show: 895 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 896 897 return tickerJSON 898 899 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 900 """ 901 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 902 903 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 904 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 905 :return: JSON formatted data with information about instrument. 906 """ 907 figiJSON = {} 908 if self.moreDebug: 909 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 910 911 if not self.figi: 912 uLogger.warning("self.figi variable is not be empty!") 913 914 else: 915 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 916 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 917 raise Exception("Instrument not allowed") 918 919 if not self.iList: 920 self.iList = self.Listing() 921 922 for item in self.iList["Shares"].keys(): 923 if self.figi == self.iList["Shares"][item]["figi"]: 924 figiJSON = self.iList["Shares"][item] 925 926 if self.moreDebug: 927 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 928 929 break 930 931 if not figiJSON: 932 for item in self.iList["Currencies"].keys(): 933 if self.figi == self.iList["Currencies"][item]["figi"]: 934 figiJSON = self.iList["Currencies"][item] 935 936 if self.moreDebug: 937 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 938 939 break 940 941 if not figiJSON: 942 for item in self.iList["Bonds"].keys(): 943 if self.figi == self.iList["Bonds"][item]["figi"]: 944 figiJSON = self.iList["Bonds"][item] 945 946 if self.moreDebug: 947 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 948 949 break 950 951 if not figiJSON: 952 for item in self.iList["Etfs"].keys(): 953 if self.figi == self.iList["Etfs"][item]["figi"]: 954 figiJSON = self.iList["Etfs"][item] 955 956 if self.moreDebug: 957 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 958 959 break 960 961 if not figiJSON: 962 for item in self.iList["Futures"].keys(): 963 if self.figi == self.iList["Futures"][item]["figi"]: 964 figiJSON = self.iList["Futures"][item] 965 966 if self.moreDebug: 967 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 968 969 break 970 971 if figiJSON: 972 self.figi = figiJSON["figi"] 973 self.ticker = figiJSON["ticker"] 974 975 if requestPrice: 976 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 977 978 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 979 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 980 981 else: 982 figiJSON["currentPrice"]["changes"] = 0 983 984 if show: 985 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 986 987 else: 988 if show: 989 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 990 991 return figiJSON 992 993 def GetCurrentPrices(self, show: bool = True) -> dict: 994 """ 995 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 996 `{"buy": [{"price": 1243.8, "quantity": 193}, 997 {"price": 1244.0, "quantity": 168}, 998 {"price": 1244.8, "quantity": 5}, 999 {"price": 1245.0, "quantity": 61}, 1000 {"price": 1245.4, "quantity": 60}], 1001 "sell": [{"price": 1243.6, "quantity": 8}, 1002 {"price": 1242.6, "quantity": 10}, 1003 {"price": 1242.4, "quantity": 18}, 1004 {"price": 1242.2, "quantity": 50}, 1005 {"price": 1242.0, "quantity": 113}], 1006 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1007 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1008 - sell: list of dicts with Buyers prices, 1009 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1010 - quantity: volume value by current price in lots, 1011 - limitUp: current trade session limit price, maximum, 1012 - limitDown: current trade session limit price, minimum, 1013 - lastPrice: last deal price of the instrument, 1014 - closePrice: previous trade session close price of the instrument. 1015 1016 See also: `SearchByTicker()` and `SearchByFIGI()`. 1017 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1018 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1019 1020 :param show: if `True` then print DOM to log and console. 1021 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1022 If an error occurred then returns an empty record: 1023 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1024 """ 1025 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1026 1027 if self.depth < 1: 1028 uLogger.error("Depth of Market (DOM) must be >=1!") 1029 raise Exception("Incorrect value") 1030 1031 if not (self.ticker or self.figi): 1032 uLogger.error("self.ticker or self.figi variables must be defined!") 1033 raise Exception("Ticker or FIGI required") 1034 1035 if self.ticker and not self.figi: 1036 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1037 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1038 1039 if not self.ticker and self.figi: 1040 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1041 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1042 1043 if not self.figi: 1044 uLogger.error("FIGI is not defined!") 1045 raise Exception("Ticker or FIGI required") 1046 1047 else: 1048 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1049 1050 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1051 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1052 self.body = str({"figi": self.figi, "depth": self.depth}) 1053 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1054 1055 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1056 # list of dicts with sellers orders: 1057 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1058 1059 # list of dicts with buyers orders: 1060 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1061 1062 # max price of instrument at this time: 1063 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1064 1065 # min price of instrument at this time: 1066 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1067 1068 # last price of deal with instrument: 1069 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1070 1071 # last close price of instrument: 1072 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1073 1074 else: 1075 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1076 uLogger.debug("Server response: {}".format(pricesResponse)) 1077 1078 if show: 1079 if prices["buy"] or prices["sell"]: 1080 info = [ 1081 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1082 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1083 self.ticker, 1084 self.figi, 1085 self.depth, 1086 ), 1087 "-" * 60, "\n", 1088 " Orders of Buyers | Orders of Sellers\n", 1089 "-" * 60, "\n", 1090 " Sell prices (volumes) | Buy prices (volumes)\n", 1091 "-" * 60, "\n", 1092 ] 1093 1094 if not prices["buy"]: 1095 info.append(" | No orders!\n") 1096 sumBuy = 0 1097 1098 else: 1099 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1100 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1101 for item in maxMinSorted: 1102 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1103 1104 if not prices["sell"]: 1105 info.append("No orders! |\n") 1106 sumSell = 0 1107 1108 else: 1109 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1110 for item in prices["sell"]: 1111 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1112 1113 info.extend([ 1114 "-" * 60, "\n", 1115 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1116 "-" * 60, "\n", 1117 ]) 1118 1119 infoText = "".join(info) 1120 1121 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1122 1123 else: 1124 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1125 1126 return prices 1127 1128 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1129 """ 1130 This method get and show information about all available broker instruments for current user account. 1131 If `instrumentsFile` string is not empty then also save information to this file. 1132 1133 :param show: if `True` then print results to console, if `False` — print only to file. 1134 :return: multi-lines string with all available broker instruments 1135 """ 1136 if not self.iList: 1137 self.iList = self.Listing() 1138 1139 info = [ 1140 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1141 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1142 ] 1143 1144 # add instruments count by type: 1145 for iType in self.iList.keys(): 1146 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1147 1148 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1149 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1150 1151 # generating info tables with all instruments by type: 1152 for iType in self.iList.keys(): 1153 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1154 1155 for instrument in self.iList[iType].keys(): 1156 iName = self.iList[iType][instrument]["name"] # instrument's name 1157 if len(iName) > 57: 1158 iName = "{}...".format(iName[:54]) # right trim for a long string 1159 1160 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1161 self.iList[iType][instrument]["ticker"], 1162 iName, 1163 self.iList[iType][instrument]["figi"], 1164 self.iList[iType][instrument]["currency"], 1165 self.iList[iType][instrument]["lot"], 1166 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1167 )) 1168 1169 infoText = "".join(info) 1170 1171 if show: 1172 uLogger.info(infoText) 1173 1174 if self.instrumentsFile: 1175 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1176 fH.write(infoText) 1177 1178 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1179 1180 return infoText 1181 1182 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1183 """ 1184 This method search and show information about instruments by part of its ticker, FIGI or name. 1185 If `searchResultsFile` string is not empty then also save information to this file. 1186 1187 :param pattern: string with part of ticker, FIGI or instrument's name. 1188 :param show: if `True` then print results to console, if `False` — return list of result only. 1189 :return: list of dictionaries with all found instruments. 1190 """ 1191 if not self.iList: 1192 self.iList = self.Listing() 1193 1194 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1195 compiledPattern = re.compile(pattern, re.IGNORECASE) 1196 1197 for iType in self.iList: 1198 for instrument in self.iList[iType].values(): 1199 searchResult = compiledPattern.search(" ".join( 1200 [instrument["ticker"], instrument["figi"], instrument["name"]] 1201 )) 1202 1203 if searchResult: 1204 searchResults[iType][instrument["ticker"]] = instrument 1205 1206 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1207 info = [ 1208 "# Search results\n\n", 1209 "* **Search pattern:** [{}]\n".format(pattern), 1210 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1211 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1212 ] 1213 infoShort = info[:] 1214 1215 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1216 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1217 skippedLine = "| ... | ... | ... | ... |\n" 1218 1219 if resultsLen == 0: 1220 info.append("\nNo results\n") 1221 infoShort.append("\nNo results\n") 1222 uLogger.warning("No results. Try changing your search pattern.") 1223 1224 else: 1225 for iType in searchResults: 1226 iTypeValuesCount = len(searchResults[iType].values()) 1227 if iTypeValuesCount > 0: 1228 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1229 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1230 1231 for instrument in searchResults[iType].values(): 1232 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1233 instrument["type"], 1234 instrument["ticker"], 1235 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1236 instrument["figi"], 1237 )) 1238 1239 if iTypeValuesCount <= 5: 1240 infoShort.extend(info[-iTypeValuesCount:]) 1241 1242 else: 1243 infoShort.extend(info[-5:]) 1244 infoShort.append(skippedLine) 1245 1246 infoText = "".join(info) 1247 infoTextShort = "".join(infoShort) 1248 1249 if show: 1250 uLogger.info(infoTextShort) 1251 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1252 1253 if self.searchResultsFile: 1254 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1255 fH.write(infoText) 1256 1257 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1258 1259 return searchResults 1260 1261 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1262 """ 1263 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1264 1265 :param instruments: list of strings with tickers or FIGIs. 1266 :return: list with unique instrument FIGIs only. 1267 """ 1268 requestedInstruments = [] 1269 for iName in instruments: 1270 if iName not in self.aliases.keys(): 1271 if iName not in requestedInstruments: 1272 requestedInstruments.append(iName) 1273 1274 else: 1275 if iName not in requestedInstruments: 1276 if self.aliases[iName] not in requestedInstruments: 1277 requestedInstruments.append(self.aliases[iName]) 1278 1279 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1280 1281 onlyUniqueFIGIs = [] 1282 for iName in requestedInstruments: 1283 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1284 continue 1285 1286 self.ticker = iName 1287 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1288 1289 if not iData: 1290 self.ticker = "" 1291 self.figi = iName 1292 1293 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1294 1295 if not iData: 1296 self.figi = "" 1297 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1298 1299 if iData and iData["figi"] not in onlyUniqueFIGIs: 1300 onlyUniqueFIGIs.append(iData["figi"]) 1301 1302 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1303 1304 return onlyUniqueFIGIs 1305 1306 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1307 """ 1308 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1309 1310 See limits: https://tinkoff.github.io/investAPI/limits/ 1311 1312 If `pricesFile` string is not empty then also save information to this file. 1313 1314 :param instruments: list of strings with tickers or FIGIs. 1315 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1316 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1317 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1318 """ 1319 if instruments is None or not instruments: 1320 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1321 raise Exception("Ticker or FIGI required") 1322 1323 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1324 1325 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1326 1327 iList = [] # trying to get info and current prices about all unique instruments: 1328 for self.figi in onlyUniqueFIGIs: 1329 iData = self.SearchByFIGI(requestPrice=True) 1330 iList.append(iData) 1331 1332 self.ShowListOfPrices(iList, show) 1333 1334 return iList 1335 1336 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1337 """ 1338 Show table contains current prices of given instruments. 1339 1340 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1341 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1342 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1343 :return: multilines text in Markdown format as a table contains current prices. 1344 """ 1345 infoText = "" 1346 1347 if show or self.pricesFile: 1348 info = [ 1349 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1350 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1351 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1352 ] 1353 1354 for item in iList: 1355 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1356 item["ticker"], 1357 item["figi"], 1358 item["type"], 1359 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1360 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1361 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1362 "{} / {}".format( 1363 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1364 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1365 ), 1366 "{} / {}".format( 1367 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1368 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1369 ), 1370 item["currency"], 1371 )) 1372 1373 infoText = "".join(info) 1374 1375 if show: 1376 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1377 1378 if self.pricesFile: 1379 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1380 fH.write(infoText) 1381 1382 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1383 1384 return infoText 1385 1386 def RequestTradingStatus(self) -> dict: 1387 """ 1388 Requesting trading status for the instrument defined by `figi` variable. 1389 1390 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1391 1392 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1393 1394 :return: dictionary with trading status attributes. Response example: 1395 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1396 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1397 """ 1398 if self.figi is None or not self.figi: 1399 uLogger.error("Variable `figi` must be defined for using this method!") 1400 raise Exception("FIGI required") 1401 1402 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1403 1404 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1405 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1406 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1407 1408 if self.moreDebug: 1409 uLogger.debug("Records about current trading status successfully received") 1410 1411 return tradingStatus 1412 1413 def RequestPortfolio(self) -> dict: 1414 """ 1415 Requesting actual user's portfolio for current `accountId`. 1416 1417 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1418 1419 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1420 1421 :return: dictionary with user's portfolio. 1422 """ 1423 if self.accountId is None or not self.accountId: 1424 uLogger.error("Variable `accountId` must be defined for using this method!") 1425 raise Exception("Account ID required") 1426 1427 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1428 1429 self.body = str({"accountId": self.accountId}) 1430 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1431 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1432 1433 if self.moreDebug: 1434 uLogger.debug("Records about user's portfolio successfully received") 1435 1436 return rawPortfolio 1437 1438 def RequestPositions(self) -> dict: 1439 """ 1440 Requesting open positions by currencies and instruments for current `accountId`. 1441 1442 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1443 1444 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1445 1446 :return: dictionary with open positions by instruments. 1447 """ 1448 if self.accountId is None or not self.accountId: 1449 uLogger.error("Variable `accountId` must be defined for using this method!") 1450 raise Exception("Account ID required") 1451 1452 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1453 1454 self.body = str({"accountId": self.accountId}) 1455 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1456 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1457 1458 if self.moreDebug: 1459 uLogger.debug("Records about current open positions successfully received") 1460 1461 return rawPositions 1462 1463 def RequestPendingOrders(self) -> list: 1464 """ 1465 Requesting current actual pending orders for current `accountId`. 1466 1467 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1468 1469 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1470 1471 :return: list of dictionaries with pending orders. 1472 """ 1473 if self.accountId is None or not self.accountId: 1474 uLogger.error("Variable `accountId` must be defined for using this method!") 1475 raise Exception("Account ID required") 1476 1477 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1478 1479 self.body = str({"accountId": self.accountId}) 1480 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1481 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1482 1483 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1484 1485 return rawOrders 1486 1487 def RequestStopOrders(self) -> list: 1488 """ 1489 Requesting current actual stop orders for current `accountId`. 1490 1491 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1492 1493 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1494 1495 :return: list of dictionaries with stop orders. 1496 """ 1497 if self.accountId is None or not self.accountId: 1498 uLogger.error("Variable `accountId` must be defined for using this method!") 1499 raise Exception("Account ID required") 1500 1501 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1502 1503 self.body = str({"accountId": self.accountId}) 1504 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1505 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1506 1507 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1508 1509 return rawStopOrders 1510 1511 def Overview(self, show: bool = False, details: str = "full") -> dict: 1512 """ 1513 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1514 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1515 and `overviewBondsCalendarFile` are defined then also save information to file. 1516 1517 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1518 many requests about the state of the portfolio, and then, based on the received data, a large number 1519 of calculation and statistics are collected. 1520 1521 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1522 :param details: how detailed should the information be? 1523 - `full` — shows full available information about portfolio status (by default), 1524 - `positions` — shows only open positions, 1525 - `orders` — shows only sections of open limits and stop orders. 1526 - `digest` — show a short digest of the portfolio status, 1527 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1528 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1529 :return: dictionary with client's raw portfolio and some statistics. 1530 """ 1531 if self.accountId is None or not self.accountId: 1532 uLogger.error("Variable `accountId` must be defined for using this method!") 1533 raise Exception("Account ID required") 1534 1535 view = { 1536 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1537 "headers": {}, # list of dictionaries, response headers without "positions" section 1538 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1539 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1540 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1541 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1542 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1543 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1544 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1545 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1546 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1547 }, 1548 "stat": { # --- some statistics calculated using "raw" sections: 1549 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1550 "availableRUB": 0., # available rubles (without other currencies) 1551 "blockedRUB": 0., # blocked sum in Russian Rouble 1552 "totalChangesRUB": 0., # changes for all open trades in RUB 1553 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1554 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1555 "sharesCostRUB": 0., # costs of all shares in RUB 1556 "bondsCostRUB": 0., # costs of all bonds in RUB 1557 "etfsCostRUB": 0., # costs of all etfs in RUB 1558 "futuresCostRUB": 0., # costs of all futures in RUB 1559 "Currencies": [], # list of dictionaries of all currencies statistics 1560 "Shares": [], # list of dictionaries of all shares statistics 1561 "Bonds": [], # list of dictionaries of all bonds statistics 1562 "Etfs": [], # list of dictionaries of all etfs statistics 1563 "Futures": [], # list of dictionaries of all futures statistics 1564 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1565 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1566 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1567 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1568 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1569 }, 1570 "analytics": { # --- some analytics of portfolio: 1571 "distrByAssets": {}, # portfolio distribution by assets 1572 "distrByCompanies": {}, # portfolio distribution by companies 1573 "distrBySectors": {}, # portfolio distribution by sectors 1574 "distrByCurrencies": {}, # portfolio distribution by currencies 1575 "distrByCountries": {}, # portfolio distribution by countries 1576 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1577 } 1578 } 1579 1580 details = details.lower() 1581 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1582 if details not in availableDetails: 1583 details = "full" 1584 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1585 1586 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1587 1588 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1589 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1590 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1591 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1592 1593 # save response headers without "positions" section: 1594 for key in portfolioResponse.keys(): 1595 if key != "positions": 1596 view["raw"]["headers"][key] = portfolioResponse[key] 1597 1598 else: 1599 continue 1600 1601 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1602 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1603 for item in portfolioResponse["positions"]: 1604 if item["instrumentType"] == "currency": 1605 self.figi = item["figi"] 1606 curr = self.SearchByFIGI(requestPrice=False) 1607 1608 # current price of currency in RUB: 1609 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1610 "name": curr["name"], 1611 "currentPrice": NanoToFloat( 1612 item["currentPrice"]["units"], 1613 item["currentPrice"]["nano"] 1614 ), 1615 } 1616 1617 view["raw"]["Currencies"].append(item) 1618 1619 elif item["instrumentType"] == "share": 1620 view["raw"]["Shares"].append(item) 1621 1622 elif item["instrumentType"] == "bond": 1623 view["raw"]["Bonds"].append(item) 1624 1625 elif item["instrumentType"] == "etf": 1626 view["raw"]["Etfs"].append(item) 1627 1628 elif item["instrumentType"] == "futures": 1629 view["raw"]["Futures"].append(item) 1630 1631 else: 1632 continue 1633 1634 # how many volume of currencies (by ISO currency name) are blocked: 1635 for item in view["raw"]["positions"]["blocked"]: 1636 blocked = NanoToFloat(item["units"], item["nano"]) 1637 if blocked > 0: 1638 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1639 1640 # how many volume of instruments (by FIGI) are blocked: 1641 for item in view["raw"]["positions"]["securities"]: 1642 blocked = int(item["blocked"]) 1643 if blocked > 0: 1644 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1645 1646 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1647 1648 if "rub" in allBlocked.keys(): 1649 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1650 1651 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1652 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1653 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1654 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1655 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1656 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1657 view["stat"]["portfolioCostRUB"] = sum([ 1658 view["stat"]["allCurrenciesCostRUB"], 1659 view["stat"]["sharesCostRUB"], 1660 view["stat"]["bondsCostRUB"], 1661 view["stat"]["etfsCostRUB"], 1662 view["stat"]["futuresCostRUB"], 1663 ]) 1664 1665 # --- calculating some portfolio statistics: 1666 byComp = {} # distribution by companies 1667 bySect = {} # distribution by sectors 1668 byCurr = {} # distribution by currencies (include RUB) 1669 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1670 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1671 1672 for item in portfolioResponse["positions"]: 1673 self.figi = item["figi"] 1674 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1675 1676 if instrument: 1677 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1678 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1679 1680 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1681 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1682 1683 else: 1684 blocked = 0 1685 1686 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1687 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1688 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1689 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1690 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1691 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1692 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1693 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1694 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1695 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1696 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1697 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1698 1699 statData = { 1700 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1701 "ticker": instrument["ticker"], # ticker by FIGI 1702 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1703 "volume": volume, # available volume of instrument 1704 "lots": lots, # volume in lots of instrument 1705 "direction": direction, # direction of an instrument's position: short or long 1706 "blocked": blocked, # blocked volume of currency or instrument 1707 "currentPrice": curPrice, # current instrument's price in basic asset 1708 "average": average, # current average position price 1709 "cost": cost, # current cost of all volume of instrument in basic asset 1710 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1711 "costRUB": costRUB, # cost of instrument in ruble 1712 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1713 "profit": profit, # expected profit at current moment 1714 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1715 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1716 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1717 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1718 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1719 "step": instrument["step"], # minimum price increment 1720 } 1721 1722 # adding distribution by unique countries: 1723 if statData["country"] not in byCountry.keys(): 1724 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1725 1726 else: 1727 byCountry[statData["country"]]["cost"] += costRUB 1728 byCountry[statData["country"]]["percent"] += percentCostRUB 1729 1730 if item["instrumentType"] != "currency": 1731 # adding distribution by unique companies: 1732 if statData["name"]: 1733 if statData["name"] not in byComp.keys(): 1734 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1735 1736 else: 1737 byComp[statData["name"]]["cost"] += costRUB 1738 byComp[statData["name"]]["percent"] += percentCostRUB 1739 1740 # adding distribution by unique sectors: 1741 if statData["sector"] not in bySect.keys(): 1742 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1743 1744 else: 1745 bySect[statData["sector"]]["cost"] += costRUB 1746 bySect[statData["sector"]]["percent"] += percentCostRUB 1747 1748 # adding distribution by unique currencies: 1749 if currency not in byCurr.keys(): 1750 byCurr[currency] = { 1751 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1752 "cost": costRUB, 1753 "percent": percentCostRUB 1754 } 1755 1756 else: 1757 byCurr[currency]["cost"] += costRUB 1758 byCurr[currency]["percent"] += percentCostRUB 1759 1760 # saving statistics for every instrument: 1761 if item["instrumentType"] == "currency": 1762 view["stat"]["Currencies"].append(statData) 1763 1764 # update dict with free funds for trading (total - blocked) by currencies 1765 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1766 view["stat"]["funds"][currency] = { 1767 "total": volume, 1768 "totalCostRUB": costRUB, # total volume cost in rubles 1769 "free": volume - blocked, 1770 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1771 } 1772 1773 elif item["instrumentType"] == "share": 1774 view["stat"]["Shares"].append(statData) 1775 1776 elif item["instrumentType"] == "bond": 1777 view["stat"]["Bonds"].append(statData) 1778 1779 elif item["instrumentType"] == "etf": 1780 view["stat"]["Etfs"].append(statData) 1781 1782 elif item["instrumentType"] == "Futures": 1783 view["stat"]["Futures"].append(statData) 1784 1785 else: 1786 continue 1787 1788 # total changes in Russian Ruble: 1789 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1790 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1791 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1792 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1793 view["stat"]["funds"]["rub"] = { 1794 "total": view["stat"]["availableRUB"], 1795 "totalCostRUB": view["stat"]["availableRUB"], 1796 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1797 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1798 } 1799 1800 # --- pending orders sector data: 1801 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1802 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1803 1804 for item in view["raw"]["orders"]: 1805 self.figi = item["figi"] 1806 1807 if item["figi"] not in uniquePendingOrdersFIGIs: 1808 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1809 1810 uniquePendingOrdersFIGIs.append(item["figi"]) 1811 uniquePendingOrders[item["figi"]] = instrument 1812 1813 else: 1814 instrument = uniquePendingOrders[item["figi"]] 1815 1816 if instrument: 1817 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1818 orderType = TKS_ORDER_TYPES[item["orderType"]] 1819 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1820 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1821 1822 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1823 if item["direction"] == "ORDER_DIRECTION_BUY": 1824 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1825 1826 else: 1827 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1828 1829 # requested price for order execution: 1830 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1831 1832 # necessary changes in percent to reach target from current price: 1833 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1834 1835 view["stat"]["orders"].append({ 1836 "orderID": item["orderId"], # orderId number parameter of current order 1837 "figi": item["figi"], # FIGI identification 1838 "ticker": instrument["ticker"], # ticker name by FIGI 1839 "lotsRequested": item["lotsRequested"], # requested lots value 1840 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1841 "currentPrice": lastPrice, # current instrument's price for defined action 1842 "targetPrice": target, # requested price for order execution in base currency 1843 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1844 "percentChanges": changes, # changes in percent to target from current price 1845 "currency": item["currency"], # instrument's currency name 1846 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1847 "type": orderType, # type of order from TKS_ORDER_TYPES 1848 "status": orderState, # order status from TKS_ORDER_STATES 1849 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1850 }) 1851 1852 # --- stop orders sector data: 1853 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1854 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1855 1856 for item in view["raw"]["stopOrders"]: 1857 self.figi = item["figi"] 1858 1859 if item["figi"] not in uniqueStopOrdersFIGIs: 1860 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1861 1862 uniqueStopOrdersFIGIs.append(item["figi"]) 1863 uniqueStopOrders[item["figi"]] = instrument 1864 1865 else: 1866 instrument = uniqueStopOrders[item["figi"]] 1867 1868 if instrument: 1869 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1870 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1871 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1872 1873 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1874 if "expirationTime" in item.keys(): 1875 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1876 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1877 1878 else: 1879 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1880 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1881 1882 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1883 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1884 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1885 1886 else: 1887 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1888 1889 # requested price when stop-order executed: 1890 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1891 1892 # price for limit-order, set up when stop-order executed: 1893 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1894 1895 # necessary changes in percent to reach target from current price: 1896 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1897 1898 view["stat"]["stopOrders"].append({ 1899 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1900 "figi": item["figi"], # FIGI identification 1901 "ticker": instrument["ticker"], # ticker name by FIGI 1902 "lotsRequested": item["lotsRequested"], # requested lots value 1903 "currentPrice": lastPrice, # current instrument's price for defined action 1904 "targetPrice": target, # requested price for stop-order execution in base currency 1905 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1906 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1907 "percentChanges": changes, # changes in percent to target from current price 1908 "currency": item["currency"], # instrument's currency name 1909 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1910 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1911 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1912 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1913 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1914 }) 1915 1916 # --- calculating data for analytics section: 1917 # portfolio distribution by assets: 1918 view["analytics"]["distrByAssets"] = { 1919 "Ruble": { 1920 "uniques": 1, 1921 "cost": view["stat"]["availableRUB"], 1922 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1923 }, 1924 "Currencies": { 1925 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1926 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1927 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1928 }, 1929 "Shares": { 1930 "uniques": len(view["stat"]["Shares"]), 1931 "cost": view["stat"]["sharesCostRUB"], 1932 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1933 }, 1934 "Bonds": { 1935 "uniques": len(view["stat"]["Bonds"]), 1936 "cost": view["stat"]["bondsCostRUB"], 1937 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1938 }, 1939 "Etfs": { 1940 "uniques": len(view["stat"]["Etfs"]), 1941 "cost": view["stat"]["etfsCostRUB"], 1942 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1943 }, 1944 "Futures": { 1945 "uniques": len(view["stat"]["Futures"]), 1946 "cost": view["stat"]["futuresCostRUB"], 1947 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1948 }, 1949 } 1950 1951 # portfolio distribution by companies: 1952 view["analytics"]["distrByCompanies"]["All money cash"] = { 1953 "ticker": "", 1954 "cost": view["stat"]["allCurrenciesCostRUB"], 1955 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1956 } 1957 view["analytics"]["distrByCompanies"].update(byComp) 1958 1959 # portfolio distribution by sectors: 1960 view["analytics"]["distrBySectors"]["All money cash"] = { 1961 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1962 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1963 } 1964 view["analytics"]["distrBySectors"].update(bySect) 1965 1966 # portfolio distribution by currencies: 1967 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1968 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1969 1970 if self.moreDebug: 1971 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1972 1973 view["analytics"]["distrByCurrencies"].update(byCurr) 1974 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1975 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1976 1977 # portfolio distribution by countries: 1978 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1979 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1980 1981 if self.moreDebug: 1982 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1983 1984 view["analytics"]["distrByCountries"].update(byCountry) 1985 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1986 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1987 1988 # --- Prepare text statistics overview in human-readable: 1989 if show: 1990 # Whatever the value `details`, header not changes: 1991 info = [ 1992 "# Client's portfolio\n\n", 1993 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1994 "* **Account ID:** [{}]\n".format(self.accountId), 1995 ] 1996 1997 if details in ["full", "positions", "digest"]: 1998 info.extend([ 1999 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2000 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2001 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2002 view["stat"]["totalChangesRUB"], 2003 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2004 view["stat"]["totalChangesPercentRUB"], 2005 ), 2006 ]) 2007 2008 if details in ["full", "positions"]: 2009 info.extend([ 2010 "## Open positions\n\n", 2011 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2012 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2013 "| Ruble | {:>31} | | | | | |\n".format( 2014 "{:.2f} ({:.2f}) rub".format( 2015 view["stat"]["availableRUB"], 2016 view["stat"]["blockedRUB"], 2017 ) 2018 ) 2019 ]) 2020 2021 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2022 return [ 2023 "| | | | | | | |\n", 2024 "| {:<27} | | | | | {:>19} | |\n".format( 2025 noTradeStr if noTradeStr else typeStr, 2026 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2027 ), 2028 ] 2029 2030 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2031 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2032 "{} [{}]".format(data["ticker"], data["figi"]), 2033 "{:.2f} ({:.2f}) {}".format( 2034 data["volume"], 2035 data["blocked"], 2036 data["currency"], 2037 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2038 data["volume"], 2039 data["blocked"], 2040 ), 2041 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2042 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2043 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2044 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2045 "{}{:.2f} {} ({}{:.2f}%)".format( 2046 "+" if data["profit"] > 0 else "", 2047 data["profit"], data["baseCurrencyName"], 2048 "+" if data["percentProfit"] > 0 else "", 2049 data["percentProfit"], 2050 ), 2051 ) 2052 2053 # --- Show currencies section: 2054 if view["stat"]["Currencies"]: 2055 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2056 for item in view["stat"]["Currencies"]: 2057 info.append(_InfoStr(item, showCurrencyName=True)) 2058 2059 else: 2060 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2061 2062 # --- Show shares section: 2063 if view["stat"]["Shares"]: 2064 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2065 2066 for item in view["stat"]["Shares"]: 2067 info.append(_InfoStr(item)) 2068 2069 else: 2070 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2071 2072 # --- Show bonds section: 2073 if view["stat"]["Bonds"]: 2074 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2075 2076 for item in view["stat"]["Bonds"]: 2077 info.append(_InfoStr(item)) 2078 2079 else: 2080 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2081 2082 # --- Show etfs section: 2083 if view["stat"]["Etfs"]: 2084 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2085 2086 for item in view["stat"]["Etfs"]: 2087 info.append(_InfoStr(item)) 2088 2089 else: 2090 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2091 2092 # --- Show futures section: 2093 if view["stat"]["Futures"]: 2094 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2095 2096 for item in view["stat"]["Futures"]: 2097 info.append(_InfoStr(item)) 2098 2099 else: 2100 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2101 2102 if details in ["full", "orders"]: 2103 # --- Show pending orders section: 2104 if view["stat"]["orders"]: 2105 info.extend([ 2106 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2107 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2108 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2109 ]) 2110 2111 for item in view["stat"]["orders"]: 2112 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2113 "{} [{}]".format(item["ticker"], item["figi"]), 2114 item["orderID"], 2115 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2116 "{} {} ({}{:.2f}%)".format( 2117 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2118 item["baseCurrencyName"], 2119 "+" if item["percentChanges"] > 0 else "", 2120 float(item["percentChanges"]), 2121 ), 2122 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2123 item["action"], 2124 item["type"], 2125 item["date"], 2126 )) 2127 2128 else: 2129 info.append("\n## Total pending limit-orders: 0\n") 2130 2131 # --- Show stop orders section: 2132 if view["stat"]["stopOrders"]: 2133 info.extend([ 2134 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2135 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2136 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2137 ]) 2138 2139 for item in view["stat"]["stopOrders"]: 2140 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2141 "{} [{}]".format(item["ticker"], item["figi"]), 2142 item["orderID"], 2143 item["lotsRequested"], 2144 "{} {} ({}{:.2f}%)".format( 2145 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2146 item["baseCurrencyName"], 2147 "+" if item["percentChanges"] > 0 else "", 2148 float(item["percentChanges"]), 2149 ), 2150 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2151 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2152 item["action"], 2153 item["type"], 2154 item["expType"], 2155 item["createDate"], 2156 item["expDate"], 2157 )) 2158 2159 else: 2160 info.append("\n## Total stop-orders: 0\n") 2161 2162 if details in ["full", "analytics"]: 2163 # -- Show analytics section: 2164 if view["stat"]["portfolioCostRUB"] > 0: 2165 info.extend([ 2166 "\n# Analytics\n" 2167 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2168 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2169 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2170 view["stat"]["totalChangesRUB"], 2171 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2172 view["stat"]["totalChangesPercentRUB"], 2173 ), 2174 "\n## Portfolio distribution by assets\n" 2175 "\n| Type | Uniques | Percent | Current cost |\n", 2176 "|------------------------------------|---------|---------|--------------------|\n", 2177 ]) 2178 2179 for key in view["analytics"]["distrByAssets"].keys(): 2180 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2181 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2182 key, 2183 view["analytics"]["distrByAssets"][key]["uniques"], 2184 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2185 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2186 )) 2187 2188 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2189 2190 info.extend([ 2191 "\n## Portfolio distribution by companies\n" 2192 "\n| Company | Percent | Current cost |\n", 2193 aSepLine, 2194 ]) 2195 2196 for company in view["analytics"]["distrByCompanies"].keys(): 2197 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2198 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2199 "{}{}".format( 2200 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2201 company, 2202 ), 2203 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2204 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2205 )) 2206 2207 info.extend([ 2208 "\n## Portfolio distribution by sectors\n" 2209 "\n| Sector | Percent | Current cost |\n", 2210 aSepLine, 2211 ]) 2212 2213 for sector in view["analytics"]["distrBySectors"].keys(): 2214 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2215 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2216 sector, 2217 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2218 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2219 )) 2220 2221 info.extend([ 2222 "\n## Portfolio distribution by currencies\n" 2223 "\n| Instruments currencies | Percent | Current cost |\n", 2224 aSepLine, 2225 ]) 2226 2227 for curr in view["analytics"]["distrByCurrencies"].keys(): 2228 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2229 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2230 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2231 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2232 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2233 )) 2234 2235 info.extend([ 2236 "\n## Portfolio distribution by countries\n" 2237 "\n| Assets by country | Percent | Current cost |\n", 2238 aSepLine, 2239 ]) 2240 2241 for country in view["analytics"]["distrByCountries"].keys(): 2242 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2243 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2244 country, 2245 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2246 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2247 )) 2248 2249 if details in ["full", "calendar"]: 2250 # -- Show bonds payment calendar section: 2251 if view["stat"]["Bonds"]: 2252 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2253 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2254 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2255 2256 else: 2257 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2258 2259 infoText = "".join(info) 2260 2261 uLogger.info(infoText) 2262 2263 if details == "full" and self.overviewFile: 2264 filename = self.overviewFile 2265 2266 elif details == "digest" and self.overviewDigestFile: 2267 filename = self.overviewDigestFile 2268 2269 elif details == "positions" and self.overviewPositionsFile: 2270 filename = self.overviewPositionsFile 2271 2272 elif details == "orders" and self.overviewOrdersFile: 2273 filename = self.overviewOrdersFile 2274 2275 elif details == "analytics" and self.overviewAnalyticsFile: 2276 filename = self.overviewAnalyticsFile 2277 2278 elif details == "calendar" and self.overviewBondsCalendarFile: 2279 filename = self.overviewBondsCalendarFile 2280 2281 else: 2282 filename = "" 2283 2284 if filename: 2285 with open(filename, "w", encoding="UTF-8") as fH: 2286 fH.write(infoText) 2287 2288 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2289 2290 return view 2291 2292 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2293 """ 2294 Returns history operations between two given dates for current `accountId`. 2295 If `reportFile` string is not empty then also save human-readable report. 2296 Shows some statistical data of closed positions. 2297 2298 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2299 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2300 :param show: if `True` then also prints all records to the console. 2301 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2302 :return: original list of dictionaries with history of deals records from API ("operations" key): 2303 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2304 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2305 """ 2306 if self.accountId is None or not self.accountId: 2307 uLogger.error("Variable `accountId` must be defined for using this method!") 2308 raise Exception("Account ID required") 2309 2310 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2311 2312 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2313 2314 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2315 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2316 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2317 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2318 customStat = {} # custom statistics in additional to responseJSON 2319 2320 # --- output report in human-readable format: 2321 if show or self.reportFile: 2322 splitLine1 = "| | | | | |\n" # Summary section 2323 splitLine2 = "| | | | | | | | |\n" # Operations section 2324 nextDay = "" 2325 2326 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2327 2328 if len(ops) > 0: 2329 customStat = { 2330 "opsCount": 0, # total operations count 2331 "buyCount": 0, # buy operations 2332 "sellCount": 0, # sell operations 2333 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2334 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2335 "payIn": {"rub": 0.}, # Deposit brokerage account 2336 "payOut": {"rub": 0.}, # Withdrawals 2337 "divs": {"rub": 0.}, # Dividends income 2338 "coupons": {"rub": 0.}, # Coupon's income 2339 "brokerCom": {"rub": 0.}, # Service commissions 2340 "serviceCom": {"rub": 0.}, # Service commissions 2341 "marginCom": {"rub": 0.}, # Margin commissions 2342 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2343 } 2344 2345 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2346 for item in ops: 2347 if item["state"] == "OPERATION_STATE_EXECUTED": 2348 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2349 2350 # count buy operations: 2351 if "_BUY" in item["operationType"]: 2352 customStat["buyCount"] += 1 2353 2354 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2355 customStat["buyTotal"][item["payment"]["currency"]] += payment 2356 2357 else: 2358 customStat["buyTotal"][item["payment"]["currency"]] = payment 2359 2360 # count sell operations: 2361 elif "_SELL" in item["operationType"]: 2362 customStat["sellCount"] += 1 2363 2364 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2365 customStat["sellTotal"][item["payment"]["currency"]] += payment 2366 2367 else: 2368 customStat["sellTotal"][item["payment"]["currency"]] = payment 2369 2370 # count incoming operations: 2371 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2372 if item["payment"]["currency"] in customStat["payIn"].keys(): 2373 customStat["payIn"][item["payment"]["currency"]] += payment 2374 2375 else: 2376 customStat["payIn"][item["payment"]["currency"]] = payment 2377 2378 # count withdrawals operations: 2379 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2380 if item["payment"]["currency"] in customStat["payOut"].keys(): 2381 customStat["payOut"][item["payment"]["currency"]] += payment 2382 2383 else: 2384 customStat["payOut"][item["payment"]["currency"]] = payment 2385 2386 # count dividends income: 2387 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2388 if item["payment"]["currency"] in customStat["divs"].keys(): 2389 customStat["divs"][item["payment"]["currency"]] += payment 2390 2391 else: 2392 customStat["divs"][item["payment"]["currency"]] = payment 2393 2394 # count coupon's income: 2395 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2396 if item["payment"]["currency"] in customStat["coupons"].keys(): 2397 customStat["coupons"][item["payment"]["currency"]] += payment 2398 2399 else: 2400 customStat["coupons"][item["payment"]["currency"]] = payment 2401 2402 # count broker commissions: 2403 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2404 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2405 customStat["brokerCom"][item["payment"]["currency"]] += payment 2406 2407 else: 2408 customStat["brokerCom"][item["payment"]["currency"]] = payment 2409 2410 # count service commissions: 2411 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2412 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2413 customStat["serviceCom"][item["payment"]["currency"]] += payment 2414 2415 else: 2416 customStat["serviceCom"][item["payment"]["currency"]] = payment 2417 2418 # count margin commissions: 2419 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2420 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2421 customStat["marginCom"][item["payment"]["currency"]] += payment 2422 2423 else: 2424 customStat["marginCom"][item["payment"]["currency"]] = payment 2425 2426 # count withholding taxes: 2427 elif "_TAX" in item["operationType"]: 2428 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2429 customStat["allTaxes"][item["payment"]["currency"]] += payment 2430 2431 else: 2432 customStat["allTaxes"][item["payment"]["currency"]] = payment 2433 2434 else: 2435 continue 2436 2437 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2438 2439 # --- view "Actions" lines: 2440 info.extend([ 2441 "| Report sections | | | | |\n", 2442 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2443 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2444 "| | Buy: {:<22} | {:<28} | | |\n".format( 2445 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2446 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2447 ), 2448 "| | Sell: {:<21} | {:<28} | | |\n".format( 2449 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2450 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2451 ), 2452 ]) 2453 2454 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2455 for key in opsKeys: 2456 if key == "rub": 2457 continue 2458 2459 info.extend([ 2460 "| | | {:<28} | | |\n".format( 2461 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2462 ), 2463 "| | | {:<28} | | |\n".format( 2464 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2465 ), 2466 ]) 2467 2468 info.append(splitLine1) 2469 2470 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2471 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2472 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2473 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2474 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2475 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2476 ) 2477 2478 # --- view "Payments" lines: 2479 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2480 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2481 2482 for key in paymentsKeys: 2483 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2484 2485 info.append(splitLine1) 2486 2487 # --- view "Commissions and taxes" lines: 2488 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2489 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2490 2491 for key in comKeys: 2492 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2493 2494 info.append(splitLine1) 2495 2496 info.extend([ 2497 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2498 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2499 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2500 ]) 2501 2502 else: 2503 info.append("Broker returned no operations during this period\n") 2504 2505 # --- view "Operations" section: 2506 for item in ops: 2507 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2508 continue 2509 2510 else: 2511 self.figi = item["figi"] if item["figi"] else "" 2512 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2513 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2514 2515 # group of deals during one day: 2516 if nextDay and item["date"].split("T")[0] != nextDay: 2517 info.append(splitLine2) 2518 nextDay = "" 2519 2520 else: 2521 nextDay = item["date"].split("T")[0] # saving current day for splitting 2522 2523 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2524 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2525 self.figi if self.figi else "—", 2526 instrument["ticker"] if instrument else "—", 2527 instrument["type"] if instrument else "—", 2528 item["quantity"] if int(item["quantity"]) > 0 else "—", 2529 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2530 TKS_OPERATION_STATES[item["state"]], 2531 TKS_OPERATION_TYPES[item["operationType"]], 2532 )) 2533 2534 infoText = "".join(info) 2535 2536 if show: 2537 if self.moreDebug: 2538 uLogger.debug("Records about history of a client's operations successfully received") 2539 2540 uLogger.info(infoText) 2541 2542 if self.reportFile: 2543 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2544 fH.write(infoText) 2545 2546 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2547 2548 return ops, customStat 2549 2550 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2551 """ 2552 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2553 2554 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2555 Warning! Broker server used ISO UTC time by default. 2556 2557 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2558 Also, `historyFile` used to update history with `onlyMissing` parameter. 2559 2560 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2561 2562 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2563 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2564 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2565 `"hour"`, `"day"`. Default: `"hour"`. 2566 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2567 False by default. Warning! History appends only from last candle to current time 2568 with always update last candle! 2569 :param csvSep: separator if csv-file is used, `,` by default. 2570 :param show: if `True` then also prints Pandas DataFrame to the console. 2571 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2572 `["date", "time", "open", "high", "low", "close", "volume"]`. 2573 """ 2574 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2575 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2576 history = None # empty pandas object for history 2577 2578 if interval not in TKS_CANDLE_INTERVALS.keys(): 2579 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2580 raise Exception("Incorrect value") 2581 2582 if not (self.ticker or self.figi): 2583 uLogger.error("Ticker or FIGI must be defined!") 2584 raise Exception("Ticker or FIGI required") 2585 2586 if self.ticker and not self.figi: 2587 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2588 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2589 2590 if self.figi and not self.ticker: 2591 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2592 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2593 2594 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2595 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2596 if interval.lower() != "day": 2597 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2598 2599 delta = dtEnd - dtStart # current UTC time minus last time in file 2600 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2601 2602 # calculate history length in candles: 2603 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2604 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2605 length += 1 # to avoid fraction time 2606 2607 # calculate data blocks count: 2608 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2609 2610 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2611 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2612 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2613 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2614 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2615 2616 tempOld = None # pandas object for old history, if --only-missing key present 2617 lastTime = None # datetime object of last old candle in file 2618 2619 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2620 uLogger.debug("--only-missing key present, add only last missing candles...") 2621 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2622 2623 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2624 2625 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2626 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2627 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2628 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2629 2630 # get last datetime object from last string in file or minus 1 delta if file is empty: 2631 if len(tempOld) > 0: 2632 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2633 2634 else: 2635 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2636 2637 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2638 2639 responseJSONs = [] # raw history blocks of data 2640 2641 blockEnd = dtEnd 2642 for item in range(blocks): 2643 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2644 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2645 2646 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2647 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2648 )) 2649 2650 if blockStart == blockEnd: 2651 uLogger.debug("Skipped this zero-length block...") 2652 2653 else: 2654 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2655 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2656 self.body = str({ 2657 "figi": self.figi, 2658 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2659 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2660 "interval": TKS_CANDLE_INTERVALS[interval][0] 2661 }) 2662 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2663 2664 if "code" in responseJSON.keys(): 2665 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2666 2667 else: 2668 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2669 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2670 2671 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2672 2673 blockEnd = blockStart 2674 2675 printCount = len(responseJSONs) # candles to show in console 2676 if responseJSONs: 2677 tempHistory = pd.DataFrame( 2678 data={ 2679 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2680 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2681 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2682 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2683 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2684 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2685 "volume": [int(item["volume"]) for item in responseJSONs], 2686 }, 2687 index=range(len(responseJSONs)), 2688 columns=["date", "time", "open", "high", "low", "close", "volume"], 2689 ) 2690 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2691 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2692 2693 # append only newest candles to old history if --only-missing key present: 2694 if onlyMissing and tempOld is not None and lastTime is not None: 2695 index = 0 # find start index in tempHistory data: 2696 2697 for i, item in tempHistory.iterrows(): 2698 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2699 2700 if curTime == lastTime: 2701 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2702 index = i 2703 printCount = index + 1 2704 break 2705 2706 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2707 2708 else: 2709 history = tempHistory # if no `--only-missing` key then load full data from server 2710 2711 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2712 2713 if history is not None and not history.empty: 2714 if show: 2715 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2716 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2717 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2718 )) 2719 2720 else: 2721 uLogger.warning("Received an empty candles history!") 2722 2723 if self.historyFile is not None: 2724 if history is not None and not history.empty: 2725 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2726 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2727 2728 else: 2729 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2730 2731 else: 2732 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2733 2734 return history 2735 2736 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2737 """ 2738 Load candles history from csv-file and return Pandas DataFrame object. 2739 2740 See also: `History()` and `ShowHistoryChart()` methods. 2741 2742 :param filePath: path to csv-file to open. 2743 """ 2744 loadedHistory = None # init candles data object 2745 2746 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2747 2748 if os.path.exists(filePath): 2749 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2750 2751 tfStr = self.priceModel.FormattedDelta( 2752 self.priceModel.timeframe, 2753 "{days} days {hours}h {minutes}m {seconds}s", 2754 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2755 self.priceModel.timeframe, 2756 "{hours}h {minutes}m {seconds}s", 2757 ) 2758 2759 if loadedHistory is not None and not loadedHistory.empty: 2760 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2761 len(loadedHistory), 2762 tfStr, 2763 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2764 ) 2765 2766 else: 2767 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2768 2769 else: 2770 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2771 2772 return loadedHistory 2773 2774 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2775 """ 2776 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2777 2778 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2779 Default: `index.html` (both for interact and non-interact candlesticks chart). 2780 2781 See also: `History()` and `LoadHistory()` methods. 2782 2783 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2784 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2785 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2786 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2787 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2788 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2789 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2790 """ 2791 if isinstance(candles, str): 2792 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2793 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2794 2795 elif isinstance(candles, pd.DataFrame): 2796 self.priceModel.prices = candles # set candles chain from variable 2797 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2798 2799 if "datetime" not in candles.columns: 2800 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2801 2802 else: 2803 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2804 raise Exception("Incorrect value") 2805 2806 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2807 2808 if interact: 2809 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2810 2811 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2812 2813 else: 2814 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2815 2816 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2817 2818 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2819 2820 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2821 """ 2822 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2823 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2824 2825 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2826 2827 :param operation: string "Buy" or "Sell". 2828 :param lots: volume, integer count of lots >= 1. 2829 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2830 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2831 :param expDate: string "Undefined" by default or local date in future, 2832 it is a string with format `%Y-%m-%d %H:%M:%S`. 2833 :return: JSON with response from broker server. 2834 """ 2835 if self.accountId is None or not self.accountId: 2836 uLogger.error("Variable `accountId` must be defined for using this method!") 2837 raise Exception("Account ID required") 2838 2839 if operation is None or not operation or operation not in ("Buy", "Sell"): 2840 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2841 raise Exception("Incorrect value") 2842 2843 if lots is None or lots < 1: 2844 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2845 lots = 1 2846 2847 if tp is None or tp < 0: 2848 tp = 0 2849 2850 if sl is None or sl < 0: 2851 sl = 0 2852 2853 if expDate is None or not expDate: 2854 expDate = "Undefined" 2855 2856 if not (self.ticker or self.figi): 2857 uLogger.error("Ticker or FIGI must be defined!") 2858 raise Exception("Ticker or FIGI required") 2859 2860 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2861 self.ticker = instrument["ticker"] 2862 self.figi = instrument["figi"] 2863 2864 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2865 2866 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2867 self.body = str({ 2868 "figi": self.figi, 2869 "quantity": str(lots), 2870 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2871 "accountId": str(self.accountId), 2872 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2873 }) 2874 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2875 2876 if "orderId" in response.keys(): 2877 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2878 operation, response["orderId"], 2879 self.ticker, self.figi, lots, 2880 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2881 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2882 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2883 )) 2884 2885 if tp > 0: 2886 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2887 2888 if sl > 0: 2889 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2890 2891 else: 2892 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2893 2894 return response 2895 2896 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2897 """ 2898 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2899 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2900 2901 See also: `Order()` and `Trade()` docstrings. 2902 2903 :param lots: volume, integer count of lots >= 1. 2904 :param tp: float > 0, take profit price of stop-order. 2905 :param sl: float > 0, stop loss price of stop-order. 2906 :param expDate: it's a local date in future. 2907 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2908 :return: JSON with response from broker server. 2909 """ 2910 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2911 2912 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2913 """ 2914 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2915 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2916 2917 See also: `Order()` and `Trade()` docstrings. 2918 2919 :param lots: volume, integer count of lots >= 1. 2920 :param tp: float > 0, take profit price of stop-order. 2921 :param sl: float > 0, stop loss price of stop-order. 2922 :param expDate: it's a local date in the future. 2923 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2924 :return: JSON with response from broker server. 2925 """ 2926 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2927 2928 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2929 """ 2930 Close position of given instruments. 2931 2932 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2933 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2934 This avoids unnecessary downloading data from the server. 2935 """ 2936 if instruments is None or not instruments: 2937 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 if isinstance(instruments, str): 2941 instruments = [instruments] 2942 2943 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2944 if uniqueInstruments: 2945 if portfolio is None or not portfolio: 2946 portfolio = self.Overview(show=False) 2947 2948 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2949 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2950 2951 for self.figi in uniqueInstruments: 2952 if self.figi not in allOpened: 2953 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2954 continue 2955 2956 # search open trade info about instrument by ticker: 2957 instrument = {} 2958 for iType in TKS_INSTRUMENTS: 2959 if instrument: 2960 break 2961 2962 for item in portfolio["stat"][iType]: 2963 if item["figi"] == self.figi: 2964 instrument = item 2965 break 2966 2967 if instrument: 2968 self.ticker = instrument["ticker"] 2969 self.figi = instrument["figi"] 2970 2971 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2972 self.ticker, 2973 self.figi, 2974 int(instrument["volume"]), 2975 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2976 )) 2977 2978 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2979 2980 if tradeLots > 0: 2981 if instrument["blocked"] > 0: 2982 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2983 instrument["blocked"], 2984 self.ticker, 2985 tradeLots, 2986 )) 2987 2988 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2989 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 2990 2991 else: 2992 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 2993 2994 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 2995 """ 2996 Close all positions of given instruments with defined type. 2997 2998 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 2999 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3000 This avoids unnecessary downloading data from the server. 3001 """ 3002 if iType not in TKS_INSTRUMENTS: 3003 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3004 3005 else: 3006 if portfolio is None or not portfolio: 3007 portfolio = self.Overview(show=False) 3008 3009 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3010 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3011 3012 if tickers and portfolio: 3013 self.CloseTrades(tickers, portfolio) 3014 3015 else: 3016 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3017 3018 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3019 """ 3020 Universal method to create market or limit orders with all available parameters for current `accountId`. 3021 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3022 3023 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3024 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3025 3026 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3027 then broker immediately open market order as you can do simple --buy or --sell operations! 3028 3029 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3030 When current price will go up or down to target price value then broker opens a limit order. 3031 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3032 3033 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3034 3035 :param operation: string "Buy" or "Sell". 3036 :param orderType: string "Limit" or "Stop". 3037 :param lots: volume, integer count of lots >= 1. 3038 :param targetPrice: target price > 0. This is open trade price for limit order. 3039 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3040 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3041 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3042 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3043 Stop loss order always executed by market price. 3044 :param expDate: string "Undefined" by default or local date in future. 3045 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3046 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3047 A limit order has no expiration date, it lasts until the end of the trading day. 3048 :return: JSON with response from broker server. 3049 """ 3050 if self.accountId is None or not self.accountId: 3051 uLogger.error("Variable `accountId` must be defined for using this method!") 3052 raise Exception("Account ID required") 3053 3054 if operation is None or not operation or operation not in ("Buy", "Sell"): 3055 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3056 raise Exception("Incorrect value") 3057 3058 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3059 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3060 raise Exception("Incorrect value") 3061 3062 if lots is None or lots < 1: 3063 uLogger.error("You must define trade volume > 0: integer count of lots!") 3064 raise Exception("Incorrect value") 3065 3066 if targetPrice is None or targetPrice <= 0: 3067 uLogger.error("Target price for limit-order must be greater than 0!") 3068 raise Exception("Incorrect value") 3069 3070 if limitPrice is None or limitPrice <= 0: 3071 limitPrice = targetPrice 3072 3073 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3074 stopType = "Limit" 3075 3076 if expDate is None or not expDate: 3077 expDate = "Undefined" 3078 3079 if not (self.ticker or self.figi): 3080 uLogger.error("Tocker or FIGI must be defined!") 3081 raise Exception("Ticker or FIGI required") 3082 3083 response = {} 3084 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3085 self.ticker = instrument["ticker"] 3086 self.figi = instrument["figi"] 3087 3088 if orderType == "Limit": 3089 uLogger.debug( 3090 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3091 self.ticker, self.figi, 3092 operation, lots, targetPrice, instrument["currency"], 3093 )) 3094 3095 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3096 self.body = str({ 3097 "figi": self.figi, 3098 "quantity": str(lots), 3099 "price": FloatToNano(targetPrice), 3100 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3101 "accountId": str(self.accountId), 3102 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3103 }) 3104 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3105 3106 if "orderId" in response.keys(): 3107 uLogger.info( 3108 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3109 response["orderId"], 3110 self.ticker, self.figi, 3111 operation, lots, targetPrice, instrument["currency"], 3112 )) 3113 3114 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3115 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3116 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3117 targetPrice, instrument["currency"], 3118 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3119 )) 3120 3121 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3122 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3123 targetPrice, instrument["currency"], 3124 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3125 )) 3126 3127 else: 3128 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3129 3130 if orderType == "Stop": 3131 uLogger.debug( 3132 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3133 self.ticker, self.figi, 3134 operation, lots, 3135 targetPrice, instrument["currency"], 3136 limitPrice, instrument["currency"], 3137 stopType, expDate, 3138 )) 3139 3140 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3141 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3142 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3143 3144 body = { 3145 "figi": self.figi, 3146 "quantity": str(lots), 3147 "price": FloatToNano(limitPrice), 3148 "stopPrice": FloatToNano(targetPrice), 3149 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3150 "accountId": str(self.accountId), 3151 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3152 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3153 } 3154 3155 if expDateUTC: 3156 body["expireDate"] = expDateUTC 3157 3158 self.body = str(body) 3159 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3160 3161 if "stopOrderId" in response.keys(): 3162 uLogger.info( 3163 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3164 response["stopOrderId"], 3165 self.ticker, self.figi, 3166 operation, lots, 3167 targetPrice, instrument["currency"], 3168 limitPrice, instrument["currency"], 3169 TKS_STOP_ORDER_TYPES[stopOrderType], 3170 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3171 )) 3172 3173 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3174 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3175 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3176 targetPrice, instrument["currency"], 3177 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3178 )) 3179 3180 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3181 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3182 targetPrice, instrument["currency"], 3183 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3184 )) 3185 3186 else: 3187 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3188 3189 return response 3190 3191 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3192 """ 3193 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3194 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3195 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3196 See also: `Order()` docstring. 3197 3198 :param lots: volume, integer count of lots >= 1. 3199 :param targetPrice: target price > 0. This is open trade price for limit order. 3200 :return: JSON with response from broker server. 3201 """ 3202 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3203 3204 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3205 """ 3206 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3207 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3208 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3209 target price value then broker opens a limit order. See also: `Order()` docstring. 3210 3211 :param lots: volume, integer count of lots >= 1. 3212 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3213 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3214 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3215 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3216 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3217 :param expDate: string "Undefined" by default or local date in future. 3218 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3219 This date is converting to UTC format for server. 3220 :return: JSON with response from broker server. 3221 """ 3222 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3223 3224 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3225 """ 3226 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3227 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3228 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3229 See also: `Order()` docstring. 3230 3231 :param lots: volume, integer count of lots >= 1. 3232 :param targetPrice: target price > 0. This is open trade price for limit order. 3233 :return: JSON with response from broker server. 3234 """ 3235 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3236 3237 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3238 """ 3239 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3240 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3241 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3242 target price value then broker opens a limit order. See also: `Order()` docstring. 3243 3244 :param lots: volume, integer count of lots >= 1. 3245 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3246 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3247 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3248 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3249 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3250 :param expDate: string "Undefined" by default or local date in future. 3251 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3252 This date is converting to UTC format for server. 3253 :return: JSON with response from broker server. 3254 """ 3255 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3256 3257 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3258 """ 3259 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3260 3261 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3262 :param allOrdersIDs: pre-received lists of all active pending orders. 3263 This avoids unnecessary downloading data from the server. 3264 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3265 """ 3266 if self.accountId is None or not self.accountId: 3267 uLogger.error("Variable `accountId` must be defined for using this method!") 3268 raise Exception("Account ID required") 3269 3270 if orderIDs: 3271 if allOrdersIDs is None or not allOrdersIDs: 3272 rawOrders = self.RequestPendingOrders() 3273 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3274 3275 if allStopOrdersIDs is None or not allStopOrdersIDs: 3276 rawStopOrders = self.RequestStopOrders() 3277 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3278 3279 for orderID in orderIDs: 3280 idInPendingOrders = orderID in allOrdersIDs 3281 idInStopOrders = orderID in allStopOrdersIDs 3282 3283 if not (idInPendingOrders or idInStopOrders): 3284 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3285 continue 3286 3287 else: 3288 if idInPendingOrders: 3289 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3290 3291 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3292 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3293 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3294 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3295 3296 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3297 if self.moreDebug: 3298 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3299 3300 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3301 3302 else: 3303 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3304 3305 elif idInStopOrders: 3306 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3307 3308 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3309 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3310 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3311 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3312 3313 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3314 if self.moreDebug: 3315 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3316 3317 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3318 3319 else: 3320 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3321 3322 else: 3323 continue 3324 3325 def CloseAllOrders(self) -> None: 3326 """ 3327 Gets a list of open pending and stop orders and cancel it all. 3328 """ 3329 rawOrders = self.RequestPendingOrders() 3330 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3331 lenOrders = len(allOrdersIDs) 3332 3333 rawStopOrders = self.RequestStopOrders() 3334 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3335 lenSOrders = len(allStopOrdersIDs) 3336 3337 if lenOrders > 0 or lenSOrders > 0: 3338 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3339 3340 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3341 3342 else: 3343 uLogger.info("Orders not found, nothing to cancel.") 3344 3345 def CloseAll(self, *args) -> None: 3346 """ 3347 Close all available (not blocked) opened trades and orders. 3348 3349 Also, you can select one or more keywords case-insensitive: 3350 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3351 3352 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3353 """ 3354 overview = self.Overview(show=False) # get all open trades info 3355 3356 if len(args) == 0: 3357 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3358 self.CloseAllOrders() # close all pending and stop orders 3359 3360 for iType in TKS_INSTRUMENTS: 3361 if iType != "Currencies": 3362 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3363 3364 else: 3365 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3366 lowerArgs = [x.lower() for x in args] 3367 3368 if "orders" in lowerArgs: 3369 self.CloseAllOrders() # close all pending and stop orders 3370 3371 for iType in TKS_INSTRUMENTS: 3372 if iType.lower() in lowerArgs and iType != "Currencies": 3373 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3374 3375 @staticmethod 3376 def ParseOrderParameters(operation, **inputParameters): 3377 """ 3378 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3379 3380 :param operation: string "Buy" or "Sell". 3381 :param inputParameters: this is dict of strings that looks like this 3382 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3383 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3384 "prices" key: one or more prices to open limit-orders 3385 Counts of values in lots and prices lists must be equals! 3386 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3387 """ 3388 # TODO: update order grid work with api v2 3389 pass 3390 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3391 # 3392 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3393 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3394 # raise Exception("Incorrect value") 3395 # 3396 # if "l" in inputParameters.keys(): 3397 # inputParameters["lots"] = inputParameters.pop("l") 3398 # 3399 # if "p" in inputParameters.keys(): 3400 # inputParameters["prices"] = inputParameters.pop("p") 3401 # 3402 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3403 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3404 # raise Exception("Incorrect value") 3405 # 3406 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3407 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3408 # 3409 # if len(lots) != len(prices): 3410 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3411 # raise Exception("Incorrect value") 3412 # 3413 # uLogger.debug("Extracted parameters for orders:") 3414 # uLogger.debug("lots = {}".format(lots)) 3415 # uLogger.debug("prices = {}".format(prices)) 3416 # 3417 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3418 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3419 # uLogger.debug("Order parameters: {}".format(result)) 3420 # 3421 # return result 3422 3423 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3424 """ 3425 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3426 3427 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3428 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3429 """ 3430 result = False 3431 msg = "Instrument not defined!" 3432 3433 if portfolio is None or not portfolio: 3434 portfolio = self.Overview(show=False) 3435 3436 if self.ticker: 3437 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3438 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3439 3440 for iType in TKS_INSTRUMENTS: 3441 for instrument in portfolio["stat"][iType]: 3442 if instrument["ticker"] == self.ticker: 3443 result = True 3444 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3445 break 3446 3447 elif self.figi: 3448 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3449 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3450 3451 for iType in TKS_INSTRUMENTS: 3452 for instrument in portfolio["stat"][iType]: 3453 if instrument["figi"] == self.figi: 3454 result = True 3455 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3456 break 3457 3458 else: 3459 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3460 3461 uLogger.debug(msg) 3462 3463 return result 3464 3465 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3466 """ 3467 Returns instrument from the user's portfolio if it presents there. 3468 Instrument must be defined by `ticker` (highly priority) or `figi`. 3469 3470 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3471 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3472 """ 3473 result = None 3474 msg = "Instrument not defined!" 3475 3476 if portfolio is None or not portfolio: 3477 portfolio = self.Overview(show=False) 3478 3479 if self.ticker: 3480 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3481 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3482 3483 for iType in TKS_INSTRUMENTS: 3484 for instrument in portfolio["stat"][iType]: 3485 if instrument["ticker"] == self.ticker: 3486 result = instrument 3487 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3488 break 3489 3490 elif self.figi: 3491 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3492 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3493 3494 for iType in TKS_INSTRUMENTS: 3495 for instrument in portfolio["stat"][iType]: 3496 if instrument["figi"] == self.figi: 3497 result = instrument 3498 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3499 break 3500 3501 else: 3502 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3503 3504 uLogger.debug(msg) 3505 3506 return result 3507 3508 def RequestLimits(self) -> dict: 3509 """ 3510 Method for obtaining the available funds for withdrawal for current `accountId`. 3511 3512 See also: 3513 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3514 - `OverviewLimits()` method 3515 3516 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3517 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3518 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3519 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3520 """ 3521 if self.accountId is None or not self.accountId: 3522 uLogger.error("Variable `accountId` must be defined for using this method!") 3523 raise Exception("Account ID required") 3524 3525 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3526 3527 self.body = str({"accountId": self.accountId}) 3528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3529 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3530 3531 if self.moreDebug: 3532 uLogger.debug("Records about available funds for withdrawal successfully received") 3533 3534 return rawLimits 3535 3536 def OverviewLimits(self, show: bool = False) -> dict: 3537 """ 3538 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3539 3540 See also: `RequestLimits()`. 3541 3542 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3543 :return: dict with raw parsed data from server and some calculated statistics about it. 3544 """ 3545 if self.accountId is None or not self.accountId: 3546 uLogger.error("Variable `accountId` must be defined for using this method!") 3547 raise Exception("Account ID required") 3548 3549 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3550 3551 view = { 3552 "rawLimits": rawLimits, 3553 "limits": { # parsed data for every currency: 3554 "money": { # this is an array of portfolio currency positions 3555 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3556 }, 3557 "blocked": { # this is an array of blocked currency 3558 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3559 }, 3560 "blockedGuarantee": { # this is locked money under collateral for futures 3561 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3562 }, 3563 }, 3564 } 3565 3566 # --- Prepare text table with limits in human-readable format: 3567 if show: 3568 info = [ 3569 "# Withdrawal limits\n\n", 3570 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3571 "* **Account ID:** [{}]\n".format(self.accountId), 3572 ] 3573 3574 if view["limits"]["money"]: 3575 info.extend([ 3576 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3577 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3578 ]) 3579 3580 else: 3581 info.append("\nNo withdrawal limits\n") 3582 3583 for curr in view["limits"]["money"].keys(): 3584 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3585 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3586 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3587 3588 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3589 "[{}]".format(curr), 3590 "{:.2f}".format(view["limits"]["money"][curr]), 3591 "{:.2f}".format(availableMoney), 3592 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3593 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3594 ) 3595 3596 if curr == "rub": 3597 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3598 3599 else: 3600 info.append(infoStr) 3601 3602 infoText = "".join(info) 3603 3604 uLogger.info(infoText) 3605 3606 if self.withdrawalLimitsFile: 3607 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3608 fH.write(infoText) 3609 3610 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3611 3612 return view 3613 3614 def RequestAccounts(self) -> dict: 3615 """ 3616 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3617 3618 See also: 3619 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3620 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3621 - `OverviewUserInfo()` method 3622 3623 :return: dict with raw data from server that contains accounts info. Example of dict: 3624 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3625 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3626 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3627 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3628 """ 3629 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3630 3631 self.body = str({}) 3632 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3633 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3634 3635 if self.moreDebug: 3636 uLogger.debug("Records about available accounts successfully received") 3637 3638 return rawAccounts 3639 3640 def RequestUserInfo(self) -> dict: 3641 """ 3642 Method for requesting common user's information. 3643 3644 See also: 3645 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3646 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3647 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3648 - `OverviewUserInfo()` method 3649 3650 :return: dict with raw data from server that contains user's information. Example of dict: 3651 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3652 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3653 """ 3654 uLogger.debug("Requesting common user's information. Wait, please...") 3655 3656 self.body = str({}) 3657 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3658 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3659 3660 if self.moreDebug: 3661 uLogger.debug("Records about current user successfully received") 3662 3663 return rawUserInfo 3664 3665 def RequestMarginStatus(self, accountId: str = None) -> dict: 3666 """ 3667 Method for requesting margin calculation for defined account ID. 3668 3669 See also: 3670 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3671 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3672 - `OverviewUserInfo()` method 3673 3674 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3675 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3676 Example of responses: 3677 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3678 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3679 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3680 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3681 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3682 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3683 """ 3684 if accountId is None or not accountId: 3685 if self.accountId is None or not self.accountId: 3686 uLogger.error("Variable `accountId` must be defined for using this method!") 3687 raise Exception("Account ID required") 3688 3689 else: 3690 accountId = self.accountId # use `self.accountId` (main ID) by default 3691 3692 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3693 3694 self.body = str({"accountId": accountId}) 3695 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3696 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3697 3698 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3699 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3700 rawMargin = {} 3701 3702 else: 3703 if self.moreDebug: 3704 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3705 3706 return rawMargin 3707 3708 def RequestTariffLimits(self) -> dict: 3709 """ 3710 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3711 3712 See also: 3713 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3714 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3715 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3716 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3717 - `OverviewUserInfo()` method 3718 3719 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3720 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3721 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3722 """ 3723 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3724 3725 self.body = str({}) 3726 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3727 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3728 3729 if self.moreDebug: 3730 uLogger.debug("Records with limits of current tariff successfully received") 3731 3732 return rawTariffLimits 3733 3734 def RequestBondCoupons(self, iJSON: dict) -> dict: 3735 """ 3736 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3737 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3738 All dates are in UTC timezone. 3739 3740 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3741 Documentation: 3742 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3743 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3744 3745 See also: `ExtendBondsData()`. 3746 3747 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3748 If raw iJSON is not data of bond then server returns an error [400] with message: 3749 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3750 :return: dictionary with bond payment calendar. Response example 3751 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3752 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3753 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3754 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3755 """ 3756 if iJSON["figi"] is None or not iJSON["figi"]: 3757 uLogger.error("FIGI must be defined for using this method!") 3758 raise Exception("FIGI required") 3759 3760 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3761 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3762 3763 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3764 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3765 self.figi, 3766 startDate, 3767 endDate, 3768 )) 3769 3770 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3771 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3772 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3773 3774 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3775 uLogger.warning("Instrument type is not bond!") 3776 3777 else: 3778 if self.moreDebug: 3779 uLogger.debug("Records about bond payment calendar successfully received") 3780 3781 return calendar 3782 3783 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3784 """ 3785 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3786 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3787 coupon yields, current yields and some statistics etc. 3788 3789 WARNING! This is too long operation if a lot of bonds requested from broker server. 3790 3791 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3792 3793 :param instruments: list of strings with tickers or FIGIs. 3794 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3795 for further used by data scientists or stock analytics. 3796 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3797 In XLSX-file and Pandas DataFrame fields mean: 3798 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3799 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3800 """ 3801 if instruments is None or not instruments: 3802 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3803 raise Exception("Ticker or FIGI required") 3804 3805 if isinstance(instruments, str): 3806 instruments = [instruments] 3807 3808 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3809 3810 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3811 3812 iCount = len(uniqueInstruments) 3813 tooLong = iCount >= 20 3814 if tooLong: 3815 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3816 3817 bonds = None 3818 for i, self.figi in enumerate(uniqueInstruments): 3819 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3820 3821 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3822 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3823 rawBond = self.SearchByFIGI(requestPrice=True) 3824 3825 # Widen raw data with UTC current time (iData["actualDateTime"]): 3826 actualDate = datetime.now(tzutc()) 3827 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3828 3829 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3830 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3831 3832 # Replace some values with human-readable: 3833 iData["nominalCurrency"] = iData["nominal"]["currency"] 3834 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3835 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3836 iData["aciCurrency"] = iData["aciValue"]["currency"] 3837 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3838 iData["issueSize"] = int(iData["issueSize"]) 3839 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3840 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3841 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3842 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3843 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3844 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3845 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3846 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3847 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3848 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3849 3850 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3851 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3852 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3853 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3854 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3855 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3856 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3857 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3858 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3859 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3860 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3861 3862 # Widen raw data with calendar data from `rawCalendar` values: 3863 calendarData = [] 3864 if "events" in iData["rawCalendar"].keys(): 3865 for item in iData["rawCalendar"]["events"]: 3866 calendarData.append({ 3867 "couponDate": item["couponDate"], 3868 "couponNumber": int(item["couponNumber"]), 3869 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3870 "payCurrency": item["payOneBond"]["currency"], 3871 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3872 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3873 "couponStartDate": item["couponStartDate"], 3874 "couponEndDate": item["couponEndDate"], 3875 "couponPeriod": item["couponPeriod"], 3876 }) 3877 3878 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3879 if "maturityDate" not in iData.keys(): 3880 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3881 3882 # Widen raw data with Coupon Rate. 3883 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3884 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3885 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3886 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3887 3888 # Widen raw data with Yield to Maturity (YTM) on current date. 3889 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3890 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3891 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3892 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3893 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3894 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3895 3896 iData["calendar"] = calendarData # adds calendar at the end 3897 3898 # Remove not used data: 3899 iData.pop("uid") 3900 iData.pop("positionUid") 3901 iData.pop("currentPrice") 3902 iData.pop("rawCalendar") 3903 3904 colNames = list(iData.keys()) 3905 if bonds is None: 3906 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3907 3908 else: 3909 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3910 3911 else: 3912 uLogger.warning("Instrument is not a bond!") 3913 3914 processed = round(100 * (i + 1) / iCount, 1) 3915 if tooLong and processed % 5 == 0: 3916 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3917 3918 else: 3919 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3920 3921 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3922 3923 # Saving bonds from Pandas DataFrame to XLSX sheet: 3924 if xlsx and self.bondsXLSXFile: 3925 with pd.ExcelWriter( 3926 path=self.bondsXLSXFile, 3927 date_format=TKS_DATE_FORMAT, 3928 datetime_format=TKS_DATE_TIME_FORMAT, 3929 mode="w", 3930 ) as writer: 3931 bonds.to_excel( 3932 writer, 3933 sheet_name="Extended bonds data", 3934 index=True, 3935 encoding="UTF-8", 3936 freeze_panes=(1, 1), 3937 ) # saving as XLSX-file with freeze first row and column as headers 3938 3939 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3940 3941 return bonds 3942 3943 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3944 """ 3945 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3946 3947 WARNING! This is too long operation if a lot of bonds requested from broker server. 3948 3949 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3950 3951 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3952 extended information about bonds: main info, current prices, bond payment calendar, 3953 coupon yields, current yields and some statistics etc. 3954 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3955 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3956 for further used by data scientists or stock analytics. 3957 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3958 """ 3959 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3960 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3961 3962 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3963 3964 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3965 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3966 calendar = None 3967 for bond in extBonds.iterrows(): 3968 for item in bond[1]["calendar"]: 3969 cData = { 3970 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3971 "couponDate": item["couponDate"], 3972 "figi": bond[1]["figi"], 3973 "ticker": bond[1]["ticker"], 3974 "name": bond[1]["name"], 3975 "couponNumber": item["couponNumber"], 3976 "payOneBond": item["payOneBond"], 3977 "payCurrency": item["payCurrency"], 3978 "couponType": item["couponType"], 3979 "couponPeriod": item["couponPeriod"], 3980 "fixDate": item["fixDate"], 3981 "couponStartDate": item["couponStartDate"], 3982 "couponEndDate": item["couponEndDate"], 3983 } 3984 3985 if calendar is None: 3986 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3987 3988 else: 3989 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 3990 3991 if calendar is not None: 3992 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 3993 3994 # Saving calendar from Pandas DataFrame to XLSX sheet: 3995 if xlsx: 3996 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 3997 3998 with pd.ExcelWriter( 3999 path=xlsxCalendarFile, 4000 date_format=TKS_DATE_FORMAT, 4001 datetime_format=TKS_DATE_TIME_FORMAT, 4002 mode="w", 4003 ) as writer: 4004 humanReadable = calendar.copy(deep=True) 4005 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4006 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4007 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4008 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4009 humanReadable.columns = colNames # human-readable column names 4010 4011 humanReadable.to_excel( 4012 writer, 4013 sheet_name="Bond payments calendar", 4014 index=False, 4015 encoding="UTF-8", 4016 freeze_panes=(1, 2), 4017 ) # saving as XLSX-file with freeze first row and column as headers 4018 4019 del humanReadable # release df in memory 4020 4021 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4022 4023 return calendar 4024 4025 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4026 """ 4027 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4028 Also, creates Markdown file with calendar data, `calendar.md` by default. 4029 4030 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4031 4032 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4033 extended information about bonds: main info, current prices, bond payment calendar, 4034 coupon yields, current yields and some statistics etc. 4035 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4036 :param show: if `True` then also printing bonds payment calendar to the console, 4037 otherwise save to file `calendarFile` only. `False` by default. 4038 :return: multilines text in Markdown format with bonds payment calendar as a table. 4039 """ 4040 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4041 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4042 4043 infoText = "# Bond payments calendar\n\n" 4044 4045 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4046 4047 if not (calendar is None or calendar.empty): 4048 splitLine = "| | | | | | | | | |\n" 4049 4050 info = [ 4051 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4052 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4053 ] 4054 4055 newMonth = False 4056 notOneBond = calendar["figi"].nunique() > 1 4057 for i, bond in enumerate(calendar.iterrows()): 4058 if newMonth and notOneBond: 4059 info.append(splitLine) 4060 4061 info.append( 4062 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4063 " √" if bond[1]["paid"] else " —", 4064 bond[1]["couponDate"].split("T")[0], 4065 bond[1]["figi"], 4066 bond[1]["ticker"], 4067 bond[1]["couponNumber"], 4068 "{} {}".format( 4069 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4070 bond[1]["payCurrency"], 4071 ), 4072 bond[1]["couponType"], 4073 bond[1]["couponPeriod"], 4074 bond[1]["fixDate"].split("T")[0], 4075 ) 4076 ) 4077 4078 if i < len(calendar.values) - 1: 4079 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4080 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4081 newMonth = False if curDate.month == nextDate.month else True 4082 4083 else: 4084 newMonth = False 4085 4086 infoText += "".join(info) 4087 4088 if show: 4089 uLogger.info("{}".format(infoText)) 4090 4091 if self.calendarFile is not None: 4092 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4093 fH.write(infoText) 4094 4095 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4096 4097 else: 4098 infoText += "No data\n" 4099 4100 return infoText 4101 4102 def OverviewAccounts(self, show: bool = False) -> dict: 4103 """ 4104 Method for parsing and show simple table with all available user accounts. 4105 4106 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4107 4108 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4109 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4110 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4111 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4112 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4113 "closed": "—", "access": "Full access" }, ...}}` 4114 """ 4115 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4116 4117 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4118 accounts = { 4119 item["id"]: { 4120 "type": TKS_ACCOUNT_TYPES[item["type"]], 4121 "name": item["name"], 4122 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4123 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4124 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4125 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4126 } for item in rawAccounts["accounts"] 4127 } 4128 4129 # Raw and parsed data with some fields replaced in "stat" section: 4130 view = { 4131 "rawAccounts": rawAccounts, 4132 "stat": accounts, 4133 } 4134 4135 # --- Prepare simple text table with only accounts data in human-readable format: 4136 if show: 4137 info = [ 4138 "# User accounts\n\n", 4139 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4140 "| Account ID | Type | Status | Name |\n", 4141 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4142 ] 4143 4144 for account in view["stat"].keys(): 4145 info.extend([ 4146 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4147 account, 4148 view["stat"][account]["type"], 4149 view["stat"][account]["status"], 4150 view["stat"][account]["name"], 4151 ) 4152 ]) 4153 4154 infoText = "".join(info) 4155 4156 uLogger.info(infoText) 4157 4158 if self.userAccountsFile: 4159 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4160 fH.write(infoText) 4161 4162 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4163 4164 return view 4165 4166 def OverviewUserInfo(self, show: bool = False) -> dict: 4167 """ 4168 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4169 4170 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4171 4172 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4173 :return: dict with raw parsed data from server and some calculated statistics about it. 4174 """ 4175 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4176 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4177 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4178 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4179 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4180 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4181 4182 # This is dict with parsed common user data: 4183 userInfo = { 4184 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4185 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4186 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4187 "tariff": rawUserInfo["tariff"], 4188 } 4189 4190 # This is an array of dict with parsed margin statuses for every account IDs: 4191 margins = {} 4192 for accountId in accounts.keys(): 4193 if rawMargins[accountId]: 4194 margins[accountId] = { 4195 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4196 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4197 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4198 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4199 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4200 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4201 } 4202 4203 else: 4204 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4205 4206 unary = {} # unary-connection limits 4207 for item in rawTariffLimits["unaryLimits"]: 4208 if item["limitPerMinute"] in unary.keys(): 4209 unary[item["limitPerMinute"]].extend(item["methods"]) 4210 4211 else: 4212 unary[item["limitPerMinute"]] = item["methods"] 4213 4214 stream = {} # stream-connection limits 4215 for item in rawTariffLimits["streamLimits"]: 4216 if item["limit"] in stream.keys(): 4217 stream[item["limit"]].extend(item["streams"]) 4218 4219 else: 4220 stream[item["limit"]] = item["streams"] 4221 4222 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4223 limits = { 4224 "unary": unary, 4225 "stream": stream, 4226 } 4227 4228 # Raw and parsed data as an output result: 4229 view = { 4230 "rawUserInfo": rawUserInfo, 4231 "rawAccounts": rawAccounts, 4232 "rawMargins": rawMargins, 4233 "rawTariffLimits": rawTariffLimits, 4234 "stat": { 4235 "userInfo": userInfo, 4236 "accounts": accounts, 4237 "margins": margins, 4238 "limits": limits, 4239 }, 4240 } 4241 4242 # --- Prepare text table with user information in human-readable format: 4243 if show: 4244 info = [ 4245 "# Full user information\n\n", 4246 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4247 "## Common information\n\n", 4248 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4249 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4250 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4251 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4252 "\n## User accounts\n\n", 4253 ] 4254 4255 for account in view["stat"]["accounts"].keys(): 4256 info.extend([ 4257 "### ID: [{}]\n\n".format(account), 4258 "| Parameters | Values |\n", 4259 "|----------------------|--------------------------------------------------------------|\n", 4260 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4261 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4262 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4263 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4264 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4265 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4266 ]) 4267 4268 if margins[account]: 4269 info.extend([ 4270 "| Margin status: | Enabled |\n", 4271 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4272 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4273 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4274 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4275 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4276 ]) 4277 4278 else: 4279 info.append("| Margin status: | Disabled |\n\n") 4280 4281 info.extend([ 4282 "\n## Current user tariff limits\n", 4283 "\nSee also:\n", 4284 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4285 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4286 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4287 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4288 "\n### Unary limits\n", 4289 ]) 4290 4291 if unary: 4292 for key, values in sorted(unary.items()): 4293 info.append("\n* Max requests per minute: {}\n".format(key)) 4294 4295 for value in values: 4296 info.append(" - {}\n".format(value)) 4297 4298 else: 4299 info.append("\nNot available\n") 4300 4301 info.append("\n### Stream limits\n") 4302 4303 if stream: 4304 for key, values in sorted(stream.items()): 4305 info.append("\n* Max stream connections: {}\n".format(key)) 4306 4307 for value in values: 4308 info.append(" - {}\n".format(value)) 4309 4310 else: 4311 info.append("\nNot available\n") 4312 4313 infoText = "".join(info) 4314 4315 uLogger.info(infoText) 4316 4317 if self.userInfoFile: 4318 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4319 fH.write(infoText) 4320 4321 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4322 4323 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
84 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 85 """ 86 Main class init. 87 88 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 89 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 90 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 91 :param useCache: use default cache file with raw data to use instead of `iList`. 92 True by default. Cache is auto-update if new day has come. 93 If you don't want to use cache and always updates raw data then set `useCache=False`. 94 :param defaultCache: path to default cache file. `dump.json` by default. 95 """ 96 if token is None or not token: 97 try: 98 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 99 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 100 101 except KeyError: 102 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 103 raise Exception("Token required") 104 105 else: 106 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 107 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 108 109 if accountId is None or not accountId: 110 try: 111 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 112 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 113 114 except KeyError: 115 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 116 117 else: 118 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 119 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 120 121 self.version = __version__ # duplicate here used TKSBrokerAPI main version 122 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 123 124 Latest version: https://pypi.org/project/tksbrokerapi/ 125 """ 126 127 self.aliases = TKS_TICKER_ALIASES 128 """Some aliases instead official tickers. 129 130 See also: `TKSEnums.TKS_TICKER_ALIASES` 131 """ 132 133 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 134 135 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 136 137 self.ticker = "" 138 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 139 140 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 141 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 142 143 See also: `SearchByTicker()`, `SearchInstruments()`. 144 """ 145 146 self.figi = "" 147 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 148 149 See also: `SearchByFIGI()`, `SearchInstruments()`. 150 """ 151 152 self.depth = 1 153 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 154 155 See also: `GetCurrentPrices()`. 156 """ 157 158 self.server = r"https://invest-public-api.tinkoff.ru/rest" 159 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 160 161 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 162 """ 163 164 uLogger.debug("Broker API server: {}".format(self.server)) 165 166 self.timeout = 15 167 """Server operations timeout in seconds. Default: `15`. 168 169 See also: `SendAPIRequest()`. 170 """ 171 172 self.headers = { 173 "Content-Type": "application/json", 174 "accept": "application/json", 175 "Authorization": "Bearer {}".format(self.token), 176 "x-app-name": "Tim55667757.TKSBrokerAPI", 177 } 178 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 179 180 See also: `SendAPIRequest()`. 181 """ 182 183 self.body = None 184 """Request body which send to broker server. Default: `None`. 185 186 See also: `SendAPIRequest()`. 187 """ 188 189 self.moreDebug = False 190 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 191 192 self.historyFile = None 193 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 194 195 See also: `History()`. 196 """ 197 198 self.htmlHistoryFile = "index.html" 199 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 200 201 See also: `ShowHistoryChart()`. 202 """ 203 204 self.instrumentsFile = "instruments.md" 205 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 206 207 See also: `ShowInstrumentsInfo()`. 208 """ 209 210 self.searchResultsFile = "search-results.md" 211 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 212 213 See also: `SearchInstruments()`. 214 """ 215 216 self.pricesFile = "prices.md" 217 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 218 219 See also: `GetListOfPrices()`. 220 """ 221 222 self.infoFile = "info.md" 223 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 224 225 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 226 """ 227 228 self.bondsXLSXFile = "ext-bonds.xlsx" 229 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 230 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 231 232 See also: `ExtendBondsData()`. 233 """ 234 235 self.calendarFile = "calendar.md" 236 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 237 238 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 239 240 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 241 """ 242 243 self.overviewFile = "overview.md" 244 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 245 246 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 247 """ 248 249 self.overviewDigestFile = "overview-digest.md" 250 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 251 252 See also: `Overview()` with parameter `details="digest"`. 253 """ 254 255 self.overviewPositionsFile = "overview-positions.md" 256 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 257 258 See also: `Overview()` with parameter `details="positions"`. 259 """ 260 261 self.overviewOrdersFile = "overview-orders.md" 262 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 263 264 See also: `Overview()` with parameter `details="orders"`. 265 """ 266 267 self.overviewAnalyticsFile = "overview-analytics.md" 268 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 269 270 See also: `Overview()` with parameter `details="analytics"`. 271 """ 272 273 self.overviewBondsCalendarFile = "overview-calendar.md" 274 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 275 276 See also: `Overview()` with parameter `details="calendar"`. 277 """ 278 279 self.reportFile = "deals.md" 280 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 281 282 See also: `Deals()`. 283 """ 284 285 self.withdrawalLimitsFile = "limits.md" 286 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 287 288 See also: `OverviewLimits()` and `RequestLimits()`. 289 """ 290 291 self.userInfoFile = "user-info.md" 292 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 293 294 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 295 """ 296 297 self.userAccountsFile = "accounts.md" 298 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 299 300 See also: `OverviewAccounts()`, `RequestAccounts()`. 301 """ 302 303 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 304 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 305 306 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 307 308 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 309 """ 310 311 self.iList = None # init iList for raw instruments data 312 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 313 314 See also: `Listing()`, `DumpInstruments()`. 315 """ 316 317 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 318 if useCache: 319 if os.path.exists(self.iListDumpFile): 320 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 321 curTime = datetime.now(tzutc()) 322 323 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 324 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 325 326 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 327 328 else: 329 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 330 331 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 332 os.path.abspath(self.iListDumpFile), 333 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 334 )) 335 336 else: 337 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 338 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 339 340 else: 341 self.iList = self.Listing() # request new raw instruments data from broker server 342 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 343 344 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 345 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 346 347 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 348 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
364 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 365 """ 366 Send GET or POST request to broker server and receive JSON object. 367 368 self.header: must be defining with dictionary of headers. 369 self.body: if define then used as request body. None by default. 370 self.timeout: global request timeout, 15 seconds by default. 371 :param url: url with REST request. 372 :param reqType: send "GET" or "POST" request. "GET" by default. 373 :param retry: how many times retry after first request if an 5xx server errors occurred. 374 :param pause: sleep time in seconds between retries. 375 :return: response JSON (dictionary) from broker. 376 """ 377 if reqType not in ("GET", "POST"): 378 uLogger.error("You can define request type: 'GET' or 'POST'!") 379 raise Exception("Incorrect value") 380 381 if self.moreDebug: 382 uLogger.debug("Request parameters:") 383 uLogger.debug(" - REST API URL: {}".format(url)) 384 uLogger.debug(" - request type: {}".format(reqType)) 385 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 386 uLogger.debug(" - body:\n{}".format(self.body)) 387 388 # fast hack to avoid all operations with some tickers/FIGI 389 responseJSON = {} 390 oK = True 391 for item in self.exclude: 392 if item in url: 393 if self.moreDebug: 394 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 395 396 oK = False 397 break 398 399 if oK: 400 counter = 0 401 response = None 402 errMsg = "" 403 404 while not response and counter <= retry: 405 if reqType == "GET": 406 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 407 408 if reqType == "POST": 409 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 410 411 if self.moreDebug: 412 uLogger.debug("Response:") 413 uLogger.debug(" - status code: {}".format(response.status_code)) 414 uLogger.debug(" - reason: {}".format(response.reason)) 415 uLogger.debug(" - body length: {}".format(len(response.text))) 416 uLogger.debug(" - headers:\n{}".format(response.headers)) 417 418 # Server returns some headers: 419 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 420 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 421 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 422 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 423 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 424 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 425 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 426 sleep(rateLimitWait) 427 428 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 429 if 400 <= response.status_code < 500: 430 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 431 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 432 counter = retry + 1 433 434 if 500 <= response.status_code < 600: 435 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 436 uLogger.debug(" - not oK, {}".format(errMsg)) 437 counter += 1 438 439 if counter <= retry: 440 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 441 sleep(pause) 442 443 responseJSON = self._ParseJSON(rawData=response.text) 444 445 if errMsg: 446 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 447 uLogger.error(" - not oK, {}".format(errMsg)) 448 449 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
482 def Listing(self) -> dict: 483 """ 484 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 485 486 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 487 """ 488 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 489 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 490 491 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 492 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 493 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 494 495 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 496 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 497 poolUpdater.close() 498 499 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 500 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 501 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 502 503 # calculate minimum price increment (step) for all instruments and set up instrument's type: 504 for iType in iList.keys(): 505 for ticker in iList[iType]: 506 iList[iType][ticker]["type"] = iType 507 508 if "minPriceIncrement" in iList[iType][ticker].keys(): 509 iList[iType][ticker]["step"] = NanoToFloat( 510 iList[iType][ticker]["minPriceIncrement"]["units"], 511 iList[iType][ticker]["minPriceIncrement"]["nano"], 512 ) 513 514 else: 515 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 516 517 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
519 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 520 """ 521 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 522 523 See also: `DumpInstruments()`, `Listing()`. 524 525 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 526 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 527 """ 528 if self.iListDumpFile is None or not self.iListDumpFile: 529 uLogger.error("Output name of dump file must be defined!") 530 raise Exception("Filename required") 531 532 if not self.iList or forceUpdate: 533 self.iList = self.Listing() 534 535 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 536 537 # Save as XLSX with separated sheets for every type of instruments: 538 with pd.ExcelWriter( 539 path=xlsxDumpFile, 540 date_format=TKS_DATE_FORMAT, 541 datetime_format=TKS_DATE_TIME_FORMAT, 542 mode="w", 543 ) as writer: 544 for iType in TKS_INSTRUMENTS: 545 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 546 df = df[sorted(df)] # sorted by column names 547 df = df.applymap( 548 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 549 na_action="ignore", 550 ) # converting numbers from nano-type to float in every cell 551 df.to_excel( 552 writer, 553 sheet_name=iType, 554 encoding="UTF-8", 555 freeze_panes=(1, 1), 556 ) # saving as XLSX-file with freeze first row and column as headers 557 558 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
560 def DumpInstruments(self, forceUpdate: bool = True) -> str: 561 """ 562 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 563 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 564 565 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 566 567 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 568 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 569 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 570 """ 571 if self.iListDumpFile is None or not self.iListDumpFile: 572 uLogger.error("Output name of dump file must be defined!") 573 raise Exception("Filename required") 574 575 if not self.iList or forceUpdate: 576 self.iList = self.Listing() 577 578 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 579 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 580 fH.write(jsonDump) 581 582 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 583 584 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
586 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 587 """ 588 Show information about one instrument defined by json data and prints it in Markdown format. 589 590 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 591 592 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 593 :param show: if `True` then also printing information about instrument and its current price. 594 :return: multilines text in Markdown format with information about one instrument. 595 """ 596 splitLine = "| | |\n" 597 infoText = "" 598 599 if iJSON is not None and iJSON and isinstance(iJSON, dict): 600 info = [ 601 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 602 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 603 "| Parameters | Values |\n", 604 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 605 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 606 "| Full name: | {:<54} |\n".format(iJSON["name"]), 607 ] 608 609 if "sector" in iJSON.keys() and iJSON["sector"]: 610 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 611 612 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 613 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 614 615 info.extend([ 616 splitLine, 617 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 618 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 619 ]) 620 621 if "isin" in iJSON.keys() and iJSON["isin"]: 622 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 623 624 if "classCode" in iJSON.keys(): 625 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 626 627 info.extend([ 628 splitLine, 629 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 630 splitLine, 631 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 632 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 633 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 634 ]) 635 636 if iJSON["figi"]: 637 self.figi = iJSON["figi"] 638 iJSON = iJSON | self.RequestTradingStatus() 639 640 info.extend([ 641 splitLine, 642 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 643 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 644 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 645 ]) 646 647 info.append(splitLine) 648 649 if "type" in iJSON.keys() and iJSON["type"]: 650 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 651 652 if "shareType" in iJSON.keys() and iJSON["shareType"]: 653 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 654 655 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 656 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 657 658 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 659 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 660 661 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 662 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 663 664 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 665 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 666 667 if "focusType" in iJSON.keys() and iJSON["focusType"]: 668 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 669 670 if "assetType" in iJSON.keys() and iJSON["assetType"]: 671 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 672 673 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 674 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 675 676 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 677 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 678 679 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 680 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 681 682 if "currency" in iJSON.keys(): 683 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 684 685 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 686 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 687 688 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 689 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 690 691 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 692 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 693 694 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 695 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 696 697 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 698 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 699 700 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 701 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 702 703 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 704 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 705 706 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 707 info.append("| Perpetual bond: | Yes |\n") 708 709 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 710 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 711 712 iExt = None 713 if iJSON["type"] == "Bonds": 714 info.extend([ 715 splitLine, 716 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 717 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 718 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 719 iJSON["nominal"]["currency"], 720 )), 721 ]) 722 723 if "floatingCouponFlag" in iJSON.keys(): 724 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 725 726 if "amortizationFlag" in iJSON.keys(): 727 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 728 729 info.append(splitLine) 730 731 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 732 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 733 734 if iJSON["figi"]: 735 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 736 737 info.extend([ 738 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 739 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 740 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 741 ]) 742 743 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 744 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 745 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 746 iJSON["aciValue"]["currency"] 747 ))) 748 749 if "currentPrice" in iJSON.keys(): 750 info.append(splitLine) 751 752 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 753 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 754 755 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 756 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 757 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 758 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 759 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 760 761 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 762 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 763 764 info.extend([ 765 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 766 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 767 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 768 )), 769 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 770 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 771 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 772 )), 773 "| Changes between last deal price and last close | {:<54} |\n".format( 774 "{:.2f}%{}".format( 775 iJSON["currentPrice"]["changes"], 776 " ({}{:.2f} {})".format( 777 "+" if bondChangesDelta > 0 else "", 778 bondChangesDelta, 779 aciCurrency 780 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 781 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 782 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 783 currency 784 ), 785 ) 786 ), 787 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 788 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 789 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 790 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 791 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 792 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 793 )), 794 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 795 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 796 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 797 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 798 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 799 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 800 )), 801 ]) 802 803 if "lot" in iJSON.keys(): 804 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 805 806 if "step" in iJSON.keys() and iJSON["step"] != 0: 807 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 808 809 # Add bond payment calendar: 810 if iJSON["type"] == "Bonds": 811 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 812 info.extend(["\n", strCalendar]) 813 814 infoText += "".join(info) 815 816 if show: 817 uLogger.info("{}".format(infoText)) 818 819 else: 820 uLogger.debug("{}".format(infoText)) 821 822 if self.infoFile is not None: 823 with open(self.infoFile, "w", encoding="UTF-8") as fH: 824 fH.write(infoText) 825 826 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 827 828 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
830 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 831 """ 832 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 833 834 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 835 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 836 :return: JSON formatted data with information about instrument. 837 """ 838 tickerJSON = {} 839 if self.moreDebug: 840 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 841 842 if not self.ticker: 843 uLogger.warning("self.ticker variable is not be empty!") 844 845 else: 846 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 847 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 848 raise Exception("Instrument not allowed") 849 850 if not self.iList: 851 self.iList = self.Listing() 852 853 if self.ticker in self.iList["Shares"].keys(): 854 tickerJSON = self.iList["Shares"][self.ticker] 855 if self.moreDebug: 856 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 857 858 elif self.ticker in self.iList["Currencies"].keys(): 859 tickerJSON = self.iList["Currencies"][self.ticker] 860 if self.moreDebug: 861 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 862 863 elif self.ticker in self.iList["Bonds"].keys(): 864 tickerJSON = self.iList["Bonds"][self.ticker] 865 if self.moreDebug: 866 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 867 868 elif self.ticker in self.iList["Etfs"].keys(): 869 tickerJSON = self.iList["Etfs"][self.ticker] 870 if self.moreDebug: 871 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 872 873 elif self.ticker in self.iList["Futures"].keys(): 874 tickerJSON = self.iList["Futures"][self.ticker] 875 if self.moreDebug: 876 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 877 878 if tickerJSON: 879 self.figi = tickerJSON["figi"] 880 881 if requestPrice: 882 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 883 884 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 885 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 886 887 else: 888 tickerJSON["currentPrice"]["changes"] = 0 889 890 if show: 891 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 892 893 else: 894 if show: 895 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 896 897 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
899 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 900 """ 901 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 902 903 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 904 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 905 :return: JSON formatted data with information about instrument. 906 """ 907 figiJSON = {} 908 if self.moreDebug: 909 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 910 911 if not self.figi: 912 uLogger.warning("self.figi variable is not be empty!") 913 914 else: 915 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 916 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 917 raise Exception("Instrument not allowed") 918 919 if not self.iList: 920 self.iList = self.Listing() 921 922 for item in self.iList["Shares"].keys(): 923 if self.figi == self.iList["Shares"][item]["figi"]: 924 figiJSON = self.iList["Shares"][item] 925 926 if self.moreDebug: 927 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 928 929 break 930 931 if not figiJSON: 932 for item in self.iList["Currencies"].keys(): 933 if self.figi == self.iList["Currencies"][item]["figi"]: 934 figiJSON = self.iList["Currencies"][item] 935 936 if self.moreDebug: 937 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 938 939 break 940 941 if not figiJSON: 942 for item in self.iList["Bonds"].keys(): 943 if self.figi == self.iList["Bonds"][item]["figi"]: 944 figiJSON = self.iList["Bonds"][item] 945 946 if self.moreDebug: 947 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 948 949 break 950 951 if not figiJSON: 952 for item in self.iList["Etfs"].keys(): 953 if self.figi == self.iList["Etfs"][item]["figi"]: 954 figiJSON = self.iList["Etfs"][item] 955 956 if self.moreDebug: 957 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 958 959 break 960 961 if not figiJSON: 962 for item in self.iList["Futures"].keys(): 963 if self.figi == self.iList["Futures"][item]["figi"]: 964 figiJSON = self.iList["Futures"][item] 965 966 if self.moreDebug: 967 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 968 969 break 970 971 if figiJSON: 972 self.figi = figiJSON["figi"] 973 self.ticker = figiJSON["ticker"] 974 975 if requestPrice: 976 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 977 978 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 979 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 980 981 else: 982 figiJSON["currentPrice"]["changes"] = 0 983 984 if show: 985 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 986 987 else: 988 if show: 989 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 990 991 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
993 def GetCurrentPrices(self, show: bool = True) -> dict: 994 """ 995 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 996 `{"buy": [{"price": 1243.8, "quantity": 193}, 997 {"price": 1244.0, "quantity": 168}, 998 {"price": 1244.8, "quantity": 5}, 999 {"price": 1245.0, "quantity": 61}, 1000 {"price": 1245.4, "quantity": 60}], 1001 "sell": [{"price": 1243.6, "quantity": 8}, 1002 {"price": 1242.6, "quantity": 10}, 1003 {"price": 1242.4, "quantity": 18}, 1004 {"price": 1242.2, "quantity": 50}, 1005 {"price": 1242.0, "quantity": 113}], 1006 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1007 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1008 - sell: list of dicts with Buyers prices, 1009 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1010 - quantity: volume value by current price in lots, 1011 - limitUp: current trade session limit price, maximum, 1012 - limitDown: current trade session limit price, minimum, 1013 - lastPrice: last deal price of the instrument, 1014 - closePrice: previous trade session close price of the instrument. 1015 1016 See also: `SearchByTicker()` and `SearchByFIGI()`. 1017 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1018 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1019 1020 :param show: if `True` then print DOM to log and console. 1021 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1022 If an error occurred then returns an empty record: 1023 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1024 """ 1025 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1026 1027 if self.depth < 1: 1028 uLogger.error("Depth of Market (DOM) must be >=1!") 1029 raise Exception("Incorrect value") 1030 1031 if not (self.ticker or self.figi): 1032 uLogger.error("self.ticker or self.figi variables must be defined!") 1033 raise Exception("Ticker or FIGI required") 1034 1035 if self.ticker and not self.figi: 1036 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1037 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1038 1039 if not self.ticker and self.figi: 1040 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1041 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1042 1043 if not self.figi: 1044 uLogger.error("FIGI is not defined!") 1045 raise Exception("Ticker or FIGI required") 1046 1047 else: 1048 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1049 1050 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1051 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1052 self.body = str({"figi": self.figi, "depth": self.depth}) 1053 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1054 1055 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1056 # list of dicts with sellers orders: 1057 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1058 1059 # list of dicts with buyers orders: 1060 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1061 1062 # max price of instrument at this time: 1063 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1064 1065 # min price of instrument at this time: 1066 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1067 1068 # last price of deal with instrument: 1069 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1070 1071 # last close price of instrument: 1072 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1073 1074 else: 1075 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1076 uLogger.debug("Server response: {}".format(pricesResponse)) 1077 1078 if show: 1079 if prices["buy"] or prices["sell"]: 1080 info = [ 1081 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1082 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1083 self.ticker, 1084 self.figi, 1085 self.depth, 1086 ), 1087 "-" * 60, "\n", 1088 " Orders of Buyers | Orders of Sellers\n", 1089 "-" * 60, "\n", 1090 " Sell prices (volumes) | Buy prices (volumes)\n", 1091 "-" * 60, "\n", 1092 ] 1093 1094 if not prices["buy"]: 1095 info.append(" | No orders!\n") 1096 sumBuy = 0 1097 1098 else: 1099 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1100 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1101 for item in maxMinSorted: 1102 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1103 1104 if not prices["sell"]: 1105 info.append("No orders! |\n") 1106 sumSell = 0 1107 1108 else: 1109 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1110 for item in prices["sell"]: 1111 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1112 1113 info.extend([ 1114 "-" * 60, "\n", 1115 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1116 "-" * 60, "\n", 1117 ]) 1118 1119 infoText = "".join(info) 1120 1121 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1122 1123 else: 1124 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1125 1126 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1128 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1129 """ 1130 This method get and show information about all available broker instruments for current user account. 1131 If `instrumentsFile` string is not empty then also save information to this file. 1132 1133 :param show: if `True` then print results to console, if `False` — print only to file. 1134 :return: multi-lines string with all available broker instruments 1135 """ 1136 if not self.iList: 1137 self.iList = self.Listing() 1138 1139 info = [ 1140 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1141 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1142 ] 1143 1144 # add instruments count by type: 1145 for iType in self.iList.keys(): 1146 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1147 1148 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1149 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1150 1151 # generating info tables with all instruments by type: 1152 for iType in self.iList.keys(): 1153 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1154 1155 for instrument in self.iList[iType].keys(): 1156 iName = self.iList[iType][instrument]["name"] # instrument's name 1157 if len(iName) > 57: 1158 iName = "{}...".format(iName[:54]) # right trim for a long string 1159 1160 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1161 self.iList[iType][instrument]["ticker"], 1162 iName, 1163 self.iList[iType][instrument]["figi"], 1164 self.iList[iType][instrument]["currency"], 1165 self.iList[iType][instrument]["lot"], 1166 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1167 )) 1168 1169 infoText = "".join(info) 1170 1171 if show: 1172 uLogger.info(infoText) 1173 1174 if self.instrumentsFile: 1175 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1176 fH.write(infoText) 1177 1178 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1179 1180 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1182 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1183 """ 1184 This method search and show information about instruments by part of its ticker, FIGI or name. 1185 If `searchResultsFile` string is not empty then also save information to this file. 1186 1187 :param pattern: string with part of ticker, FIGI or instrument's name. 1188 :param show: if `True` then print results to console, if `False` — return list of result only. 1189 :return: list of dictionaries with all found instruments. 1190 """ 1191 if not self.iList: 1192 self.iList = self.Listing() 1193 1194 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1195 compiledPattern = re.compile(pattern, re.IGNORECASE) 1196 1197 for iType in self.iList: 1198 for instrument in self.iList[iType].values(): 1199 searchResult = compiledPattern.search(" ".join( 1200 [instrument["ticker"], instrument["figi"], instrument["name"]] 1201 )) 1202 1203 if searchResult: 1204 searchResults[iType][instrument["ticker"]] = instrument 1205 1206 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1207 info = [ 1208 "# Search results\n\n", 1209 "* **Search pattern:** [{}]\n".format(pattern), 1210 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1211 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1212 ] 1213 infoShort = info[:] 1214 1215 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1216 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1217 skippedLine = "| ... | ... | ... | ... |\n" 1218 1219 if resultsLen == 0: 1220 info.append("\nNo results\n") 1221 infoShort.append("\nNo results\n") 1222 uLogger.warning("No results. Try changing your search pattern.") 1223 1224 else: 1225 for iType in searchResults: 1226 iTypeValuesCount = len(searchResults[iType].values()) 1227 if iTypeValuesCount > 0: 1228 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1229 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1230 1231 for instrument in searchResults[iType].values(): 1232 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1233 instrument["type"], 1234 instrument["ticker"], 1235 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1236 instrument["figi"], 1237 )) 1238 1239 if iTypeValuesCount <= 5: 1240 infoShort.extend(info[-iTypeValuesCount:]) 1241 1242 else: 1243 infoShort.extend(info[-5:]) 1244 infoShort.append(skippedLine) 1245 1246 infoText = "".join(info) 1247 infoTextShort = "".join(infoShort) 1248 1249 if show: 1250 uLogger.info(infoTextShort) 1251 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1252 1253 if self.searchResultsFile: 1254 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1255 fH.write(infoText) 1256 1257 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1258 1259 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1261 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1262 """ 1263 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1264 1265 :param instruments: list of strings with tickers or FIGIs. 1266 :return: list with unique instrument FIGIs only. 1267 """ 1268 requestedInstruments = [] 1269 for iName in instruments: 1270 if iName not in self.aliases.keys(): 1271 if iName not in requestedInstruments: 1272 requestedInstruments.append(iName) 1273 1274 else: 1275 if iName not in requestedInstruments: 1276 if self.aliases[iName] not in requestedInstruments: 1277 requestedInstruments.append(self.aliases[iName]) 1278 1279 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1280 1281 onlyUniqueFIGIs = [] 1282 for iName in requestedInstruments: 1283 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1284 continue 1285 1286 self.ticker = iName 1287 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1288 1289 if not iData: 1290 self.ticker = "" 1291 self.figi = iName 1292 1293 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1294 1295 if not iData: 1296 self.figi = "" 1297 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1298 1299 if iData and iData["figi"] not in onlyUniqueFIGIs: 1300 onlyUniqueFIGIs.append(iData["figi"]) 1301 1302 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1303 1304 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1306 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1307 """ 1308 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1309 1310 See limits: https://tinkoff.github.io/investAPI/limits/ 1311 1312 If `pricesFile` string is not empty then also save information to this file. 1313 1314 :param instruments: list of strings with tickers or FIGIs. 1315 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1316 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1317 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1318 """ 1319 if instruments is None or not instruments: 1320 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1321 raise Exception("Ticker or FIGI required") 1322 1323 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1324 1325 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1326 1327 iList = [] # trying to get info and current prices about all unique instruments: 1328 for self.figi in onlyUniqueFIGIs: 1329 iData = self.SearchByFIGI(requestPrice=True) 1330 iList.append(iData) 1331 1332 self.ShowListOfPrices(iList, show) 1333 1334 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1336 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1337 """ 1338 Show table contains current prices of given instruments. 1339 1340 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1341 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1342 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1343 :return: multilines text in Markdown format as a table contains current prices. 1344 """ 1345 infoText = "" 1346 1347 if show or self.pricesFile: 1348 info = [ 1349 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1350 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1351 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1352 ] 1353 1354 for item in iList: 1355 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1356 item["ticker"], 1357 item["figi"], 1358 item["type"], 1359 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1360 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1361 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1362 "{} / {}".format( 1363 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1364 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1365 ), 1366 "{} / {}".format( 1367 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1368 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1369 ), 1370 item["currency"], 1371 )) 1372 1373 infoText = "".join(info) 1374 1375 if show: 1376 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1377 1378 if self.pricesFile: 1379 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1380 fH.write(infoText) 1381 1382 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1383 1384 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1386 def RequestTradingStatus(self) -> dict: 1387 """ 1388 Requesting trading status for the instrument defined by `figi` variable. 1389 1390 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1391 1392 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1393 1394 :return: dictionary with trading status attributes. Response example: 1395 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1396 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1397 """ 1398 if self.figi is None or not self.figi: 1399 uLogger.error("Variable `figi` must be defined for using this method!") 1400 raise Exception("FIGI required") 1401 1402 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1403 1404 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1405 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1406 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1407 1408 if self.moreDebug: 1409 uLogger.debug("Records about current trading status successfully received") 1410 1411 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1413 def RequestPortfolio(self) -> dict: 1414 """ 1415 Requesting actual user's portfolio for current `accountId`. 1416 1417 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1418 1419 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1420 1421 :return: dictionary with user's portfolio. 1422 """ 1423 if self.accountId is None or not self.accountId: 1424 uLogger.error("Variable `accountId` must be defined for using this method!") 1425 raise Exception("Account ID required") 1426 1427 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1428 1429 self.body = str({"accountId": self.accountId}) 1430 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1431 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1432 1433 if self.moreDebug: 1434 uLogger.debug("Records about user's portfolio successfully received") 1435 1436 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1438 def RequestPositions(self) -> dict: 1439 """ 1440 Requesting open positions by currencies and instruments for current `accountId`. 1441 1442 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1443 1444 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1445 1446 :return: dictionary with open positions by instruments. 1447 """ 1448 if self.accountId is None or not self.accountId: 1449 uLogger.error("Variable `accountId` must be defined for using this method!") 1450 raise Exception("Account ID required") 1451 1452 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1453 1454 self.body = str({"accountId": self.accountId}) 1455 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1456 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1457 1458 if self.moreDebug: 1459 uLogger.debug("Records about current open positions successfully received") 1460 1461 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1463 def RequestPendingOrders(self) -> list: 1464 """ 1465 Requesting current actual pending orders for current `accountId`. 1466 1467 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1468 1469 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1470 1471 :return: list of dictionaries with pending orders. 1472 """ 1473 if self.accountId is None or not self.accountId: 1474 uLogger.error("Variable `accountId` must be defined for using this method!") 1475 raise Exception("Account ID required") 1476 1477 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1478 1479 self.body = str({"accountId": self.accountId}) 1480 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1481 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1482 1483 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1484 1485 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1487 def RequestStopOrders(self) -> list: 1488 """ 1489 Requesting current actual stop orders for current `accountId`. 1490 1491 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1492 1493 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1494 1495 :return: list of dictionaries with stop orders. 1496 """ 1497 if self.accountId is None or not self.accountId: 1498 uLogger.error("Variable `accountId` must be defined for using this method!") 1499 raise Exception("Account ID required") 1500 1501 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1502 1503 self.body = str({"accountId": self.accountId}) 1504 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1505 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1506 1507 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1508 1509 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1511 def Overview(self, show: bool = False, details: str = "full") -> dict: 1512 """ 1513 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1514 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1515 and `overviewBondsCalendarFile` are defined then also save information to file. 1516 1517 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1518 many requests about the state of the portfolio, and then, based on the received data, a large number 1519 of calculation and statistics are collected. 1520 1521 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1522 :param details: how detailed should the information be? 1523 - `full` — shows full available information about portfolio status (by default), 1524 - `positions` — shows only open positions, 1525 - `orders` — shows only sections of open limits and stop orders. 1526 - `digest` — show a short digest of the portfolio status, 1527 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1528 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1529 :return: dictionary with client's raw portfolio and some statistics. 1530 """ 1531 if self.accountId is None or not self.accountId: 1532 uLogger.error("Variable `accountId` must be defined for using this method!") 1533 raise Exception("Account ID required") 1534 1535 view = { 1536 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1537 "headers": {}, # list of dictionaries, response headers without "positions" section 1538 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1539 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1540 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1541 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1542 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1543 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1544 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1545 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1546 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1547 }, 1548 "stat": { # --- some statistics calculated using "raw" sections: 1549 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1550 "availableRUB": 0., # available rubles (without other currencies) 1551 "blockedRUB": 0., # blocked sum in Russian Rouble 1552 "totalChangesRUB": 0., # changes for all open trades in RUB 1553 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1554 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1555 "sharesCostRUB": 0., # costs of all shares in RUB 1556 "bondsCostRUB": 0., # costs of all bonds in RUB 1557 "etfsCostRUB": 0., # costs of all etfs in RUB 1558 "futuresCostRUB": 0., # costs of all futures in RUB 1559 "Currencies": [], # list of dictionaries of all currencies statistics 1560 "Shares": [], # list of dictionaries of all shares statistics 1561 "Bonds": [], # list of dictionaries of all bonds statistics 1562 "Etfs": [], # list of dictionaries of all etfs statistics 1563 "Futures": [], # list of dictionaries of all futures statistics 1564 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1565 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1566 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1567 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1568 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1569 }, 1570 "analytics": { # --- some analytics of portfolio: 1571 "distrByAssets": {}, # portfolio distribution by assets 1572 "distrByCompanies": {}, # portfolio distribution by companies 1573 "distrBySectors": {}, # portfolio distribution by sectors 1574 "distrByCurrencies": {}, # portfolio distribution by currencies 1575 "distrByCountries": {}, # portfolio distribution by countries 1576 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1577 } 1578 } 1579 1580 details = details.lower() 1581 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1582 if details not in availableDetails: 1583 details = "full" 1584 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1585 1586 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1587 1588 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1589 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1590 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1591 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1592 1593 # save response headers without "positions" section: 1594 for key in portfolioResponse.keys(): 1595 if key != "positions": 1596 view["raw"]["headers"][key] = portfolioResponse[key] 1597 1598 else: 1599 continue 1600 1601 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1602 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1603 for item in portfolioResponse["positions"]: 1604 if item["instrumentType"] == "currency": 1605 self.figi = item["figi"] 1606 curr = self.SearchByFIGI(requestPrice=False) 1607 1608 # current price of currency in RUB: 1609 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1610 "name": curr["name"], 1611 "currentPrice": NanoToFloat( 1612 item["currentPrice"]["units"], 1613 item["currentPrice"]["nano"] 1614 ), 1615 } 1616 1617 view["raw"]["Currencies"].append(item) 1618 1619 elif item["instrumentType"] == "share": 1620 view["raw"]["Shares"].append(item) 1621 1622 elif item["instrumentType"] == "bond": 1623 view["raw"]["Bonds"].append(item) 1624 1625 elif item["instrumentType"] == "etf": 1626 view["raw"]["Etfs"].append(item) 1627 1628 elif item["instrumentType"] == "futures": 1629 view["raw"]["Futures"].append(item) 1630 1631 else: 1632 continue 1633 1634 # how many volume of currencies (by ISO currency name) are blocked: 1635 for item in view["raw"]["positions"]["blocked"]: 1636 blocked = NanoToFloat(item["units"], item["nano"]) 1637 if blocked > 0: 1638 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1639 1640 # how many volume of instruments (by FIGI) are blocked: 1641 for item in view["raw"]["positions"]["securities"]: 1642 blocked = int(item["blocked"]) 1643 if blocked > 0: 1644 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1645 1646 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1647 1648 if "rub" in allBlocked.keys(): 1649 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1650 1651 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1652 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1653 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1654 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1655 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1656 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1657 view["stat"]["portfolioCostRUB"] = sum([ 1658 view["stat"]["allCurrenciesCostRUB"], 1659 view["stat"]["sharesCostRUB"], 1660 view["stat"]["bondsCostRUB"], 1661 view["stat"]["etfsCostRUB"], 1662 view["stat"]["futuresCostRUB"], 1663 ]) 1664 1665 # --- calculating some portfolio statistics: 1666 byComp = {} # distribution by companies 1667 bySect = {} # distribution by sectors 1668 byCurr = {} # distribution by currencies (include RUB) 1669 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1670 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1671 1672 for item in portfolioResponse["positions"]: 1673 self.figi = item["figi"] 1674 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1675 1676 if instrument: 1677 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1678 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1679 1680 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1681 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1682 1683 else: 1684 blocked = 0 1685 1686 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1687 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1688 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1689 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1690 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1691 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1692 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1693 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1694 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1695 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1696 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1697 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1698 1699 statData = { 1700 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1701 "ticker": instrument["ticker"], # ticker by FIGI 1702 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1703 "volume": volume, # available volume of instrument 1704 "lots": lots, # volume in lots of instrument 1705 "direction": direction, # direction of an instrument's position: short or long 1706 "blocked": blocked, # blocked volume of currency or instrument 1707 "currentPrice": curPrice, # current instrument's price in basic asset 1708 "average": average, # current average position price 1709 "cost": cost, # current cost of all volume of instrument in basic asset 1710 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1711 "costRUB": costRUB, # cost of instrument in ruble 1712 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1713 "profit": profit, # expected profit at current moment 1714 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1715 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1716 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1717 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1718 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1719 "step": instrument["step"], # minimum price increment 1720 } 1721 1722 # adding distribution by unique countries: 1723 if statData["country"] not in byCountry.keys(): 1724 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1725 1726 else: 1727 byCountry[statData["country"]]["cost"] += costRUB 1728 byCountry[statData["country"]]["percent"] += percentCostRUB 1729 1730 if item["instrumentType"] != "currency": 1731 # adding distribution by unique companies: 1732 if statData["name"]: 1733 if statData["name"] not in byComp.keys(): 1734 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1735 1736 else: 1737 byComp[statData["name"]]["cost"] += costRUB 1738 byComp[statData["name"]]["percent"] += percentCostRUB 1739 1740 # adding distribution by unique sectors: 1741 if statData["sector"] not in bySect.keys(): 1742 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1743 1744 else: 1745 bySect[statData["sector"]]["cost"] += costRUB 1746 bySect[statData["sector"]]["percent"] += percentCostRUB 1747 1748 # adding distribution by unique currencies: 1749 if currency not in byCurr.keys(): 1750 byCurr[currency] = { 1751 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1752 "cost": costRUB, 1753 "percent": percentCostRUB 1754 } 1755 1756 else: 1757 byCurr[currency]["cost"] += costRUB 1758 byCurr[currency]["percent"] += percentCostRUB 1759 1760 # saving statistics for every instrument: 1761 if item["instrumentType"] == "currency": 1762 view["stat"]["Currencies"].append(statData) 1763 1764 # update dict with free funds for trading (total - blocked) by currencies 1765 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1766 view["stat"]["funds"][currency] = { 1767 "total": volume, 1768 "totalCostRUB": costRUB, # total volume cost in rubles 1769 "free": volume - blocked, 1770 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1771 } 1772 1773 elif item["instrumentType"] == "share": 1774 view["stat"]["Shares"].append(statData) 1775 1776 elif item["instrumentType"] == "bond": 1777 view["stat"]["Bonds"].append(statData) 1778 1779 elif item["instrumentType"] == "etf": 1780 view["stat"]["Etfs"].append(statData) 1781 1782 elif item["instrumentType"] == "Futures": 1783 view["stat"]["Futures"].append(statData) 1784 1785 else: 1786 continue 1787 1788 # total changes in Russian Ruble: 1789 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1790 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1791 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1792 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1793 view["stat"]["funds"]["rub"] = { 1794 "total": view["stat"]["availableRUB"], 1795 "totalCostRUB": view["stat"]["availableRUB"], 1796 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1797 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1798 } 1799 1800 # --- pending orders sector data: 1801 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1802 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1803 1804 for item in view["raw"]["orders"]: 1805 self.figi = item["figi"] 1806 1807 if item["figi"] not in uniquePendingOrdersFIGIs: 1808 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1809 1810 uniquePendingOrdersFIGIs.append(item["figi"]) 1811 uniquePendingOrders[item["figi"]] = instrument 1812 1813 else: 1814 instrument = uniquePendingOrders[item["figi"]] 1815 1816 if instrument: 1817 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1818 orderType = TKS_ORDER_TYPES[item["orderType"]] 1819 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1820 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1821 1822 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1823 if item["direction"] == "ORDER_DIRECTION_BUY": 1824 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1825 1826 else: 1827 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1828 1829 # requested price for order execution: 1830 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1831 1832 # necessary changes in percent to reach target from current price: 1833 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1834 1835 view["stat"]["orders"].append({ 1836 "orderID": item["orderId"], # orderId number parameter of current order 1837 "figi": item["figi"], # FIGI identification 1838 "ticker": instrument["ticker"], # ticker name by FIGI 1839 "lotsRequested": item["lotsRequested"], # requested lots value 1840 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1841 "currentPrice": lastPrice, # current instrument's price for defined action 1842 "targetPrice": target, # requested price for order execution in base currency 1843 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1844 "percentChanges": changes, # changes in percent to target from current price 1845 "currency": item["currency"], # instrument's currency name 1846 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1847 "type": orderType, # type of order from TKS_ORDER_TYPES 1848 "status": orderState, # order status from TKS_ORDER_STATES 1849 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1850 }) 1851 1852 # --- stop orders sector data: 1853 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1854 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1855 1856 for item in view["raw"]["stopOrders"]: 1857 self.figi = item["figi"] 1858 1859 if item["figi"] not in uniqueStopOrdersFIGIs: 1860 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1861 1862 uniqueStopOrdersFIGIs.append(item["figi"]) 1863 uniqueStopOrders[item["figi"]] = instrument 1864 1865 else: 1866 instrument = uniqueStopOrders[item["figi"]] 1867 1868 if instrument: 1869 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1870 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1871 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1872 1873 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1874 if "expirationTime" in item.keys(): 1875 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1876 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1877 1878 else: 1879 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1880 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1881 1882 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1883 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1884 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1885 1886 else: 1887 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1888 1889 # requested price when stop-order executed: 1890 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1891 1892 # price for limit-order, set up when stop-order executed: 1893 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1894 1895 # necessary changes in percent to reach target from current price: 1896 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1897 1898 view["stat"]["stopOrders"].append({ 1899 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1900 "figi": item["figi"], # FIGI identification 1901 "ticker": instrument["ticker"], # ticker name by FIGI 1902 "lotsRequested": item["lotsRequested"], # requested lots value 1903 "currentPrice": lastPrice, # current instrument's price for defined action 1904 "targetPrice": target, # requested price for stop-order execution in base currency 1905 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1906 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1907 "percentChanges": changes, # changes in percent to target from current price 1908 "currency": item["currency"], # instrument's currency name 1909 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1910 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1911 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1912 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1913 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1914 }) 1915 1916 # --- calculating data for analytics section: 1917 # portfolio distribution by assets: 1918 view["analytics"]["distrByAssets"] = { 1919 "Ruble": { 1920 "uniques": 1, 1921 "cost": view["stat"]["availableRUB"], 1922 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1923 }, 1924 "Currencies": { 1925 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1926 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1927 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1928 }, 1929 "Shares": { 1930 "uniques": len(view["stat"]["Shares"]), 1931 "cost": view["stat"]["sharesCostRUB"], 1932 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1933 }, 1934 "Bonds": { 1935 "uniques": len(view["stat"]["Bonds"]), 1936 "cost": view["stat"]["bondsCostRUB"], 1937 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1938 }, 1939 "Etfs": { 1940 "uniques": len(view["stat"]["Etfs"]), 1941 "cost": view["stat"]["etfsCostRUB"], 1942 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1943 }, 1944 "Futures": { 1945 "uniques": len(view["stat"]["Futures"]), 1946 "cost": view["stat"]["futuresCostRUB"], 1947 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1948 }, 1949 } 1950 1951 # portfolio distribution by companies: 1952 view["analytics"]["distrByCompanies"]["All money cash"] = { 1953 "ticker": "", 1954 "cost": view["stat"]["allCurrenciesCostRUB"], 1955 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1956 } 1957 view["analytics"]["distrByCompanies"].update(byComp) 1958 1959 # portfolio distribution by sectors: 1960 view["analytics"]["distrBySectors"]["All money cash"] = { 1961 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 1962 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 1963 } 1964 view["analytics"]["distrBySectors"].update(bySect) 1965 1966 # portfolio distribution by currencies: 1967 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 1968 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 1969 1970 if self.moreDebug: 1971 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 1972 1973 view["analytics"]["distrByCurrencies"].update(byCurr) 1974 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1975 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1976 1977 # portfolio distribution by countries: 1978 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 1979 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 1980 1981 if self.moreDebug: 1982 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 1983 1984 view["analytics"]["distrByCountries"].update(byCountry) 1985 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 1986 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 1987 1988 # --- Prepare text statistics overview in human-readable: 1989 if show: 1990 # Whatever the value `details`, header not changes: 1991 info = [ 1992 "# Client's portfolio\n\n", 1993 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1994 "* **Account ID:** [{}]\n".format(self.accountId), 1995 ] 1996 1997 if details in ["full", "positions", "digest"]: 1998 info.extend([ 1999 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2000 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2001 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2002 view["stat"]["totalChangesRUB"], 2003 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2004 view["stat"]["totalChangesPercentRUB"], 2005 ), 2006 ]) 2007 2008 if details in ["full", "positions"]: 2009 info.extend([ 2010 "## Open positions\n\n", 2011 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2012 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2013 "| Ruble | {:>31} | | | | | |\n".format( 2014 "{:.2f} ({:.2f}) rub".format( 2015 view["stat"]["availableRUB"], 2016 view["stat"]["blockedRUB"], 2017 ) 2018 ) 2019 ]) 2020 2021 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2022 return [ 2023 "| | | | | | | |\n", 2024 "| {:<27} | | | | | {:>19} | |\n".format( 2025 noTradeStr if noTradeStr else typeStr, 2026 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2027 ), 2028 ] 2029 2030 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2031 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2032 "{} [{}]".format(data["ticker"], data["figi"]), 2033 "{:.2f} ({:.2f}) {}".format( 2034 data["volume"], 2035 data["blocked"], 2036 data["currency"], 2037 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2038 data["volume"], 2039 data["blocked"], 2040 ), 2041 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2042 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2043 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2044 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2045 "{}{:.2f} {} ({}{:.2f}%)".format( 2046 "+" if data["profit"] > 0 else "", 2047 data["profit"], data["baseCurrencyName"], 2048 "+" if data["percentProfit"] > 0 else "", 2049 data["percentProfit"], 2050 ), 2051 ) 2052 2053 # --- Show currencies section: 2054 if view["stat"]["Currencies"]: 2055 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2056 for item in view["stat"]["Currencies"]: 2057 info.append(_InfoStr(item, showCurrencyName=True)) 2058 2059 else: 2060 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2061 2062 # --- Show shares section: 2063 if view["stat"]["Shares"]: 2064 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2065 2066 for item in view["stat"]["Shares"]: 2067 info.append(_InfoStr(item)) 2068 2069 else: 2070 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2071 2072 # --- Show bonds section: 2073 if view["stat"]["Bonds"]: 2074 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2075 2076 for item in view["stat"]["Bonds"]: 2077 info.append(_InfoStr(item)) 2078 2079 else: 2080 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2081 2082 # --- Show etfs section: 2083 if view["stat"]["Etfs"]: 2084 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2085 2086 for item in view["stat"]["Etfs"]: 2087 info.append(_InfoStr(item)) 2088 2089 else: 2090 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2091 2092 # --- Show futures section: 2093 if view["stat"]["Futures"]: 2094 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2095 2096 for item in view["stat"]["Futures"]: 2097 info.append(_InfoStr(item)) 2098 2099 else: 2100 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2101 2102 if details in ["full", "orders"]: 2103 # --- Show pending orders section: 2104 if view["stat"]["orders"]: 2105 info.extend([ 2106 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2107 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2108 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2109 ]) 2110 2111 for item in view["stat"]["orders"]: 2112 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2113 "{} [{}]".format(item["ticker"], item["figi"]), 2114 item["orderID"], 2115 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2116 "{} {} ({}{:.2f}%)".format( 2117 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2118 item["baseCurrencyName"], 2119 "+" if item["percentChanges"] > 0 else "", 2120 float(item["percentChanges"]), 2121 ), 2122 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2123 item["action"], 2124 item["type"], 2125 item["date"], 2126 )) 2127 2128 else: 2129 info.append("\n## Total pending limit-orders: 0\n") 2130 2131 # --- Show stop orders section: 2132 if view["stat"]["stopOrders"]: 2133 info.extend([ 2134 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2135 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2136 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2137 ]) 2138 2139 for item in view["stat"]["stopOrders"]: 2140 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2141 "{} [{}]".format(item["ticker"], item["figi"]), 2142 item["orderID"], 2143 item["lotsRequested"], 2144 "{} {} ({}{:.2f}%)".format( 2145 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2146 item["baseCurrencyName"], 2147 "+" if item["percentChanges"] > 0 else "", 2148 float(item["percentChanges"]), 2149 ), 2150 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2151 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2152 item["action"], 2153 item["type"], 2154 item["expType"], 2155 item["createDate"], 2156 item["expDate"], 2157 )) 2158 2159 else: 2160 info.append("\n## Total stop-orders: 0\n") 2161 2162 if details in ["full", "analytics"]: 2163 # -- Show analytics section: 2164 if view["stat"]["portfolioCostRUB"] > 0: 2165 info.extend([ 2166 "\n# Analytics\n" 2167 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2168 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2169 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2170 view["stat"]["totalChangesRUB"], 2171 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2172 view["stat"]["totalChangesPercentRUB"], 2173 ), 2174 "\n## Portfolio distribution by assets\n" 2175 "\n| Type | Uniques | Percent | Current cost |\n", 2176 "|------------------------------------|---------|---------|--------------------|\n", 2177 ]) 2178 2179 for key in view["analytics"]["distrByAssets"].keys(): 2180 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2181 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2182 key, 2183 view["analytics"]["distrByAssets"][key]["uniques"], 2184 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2185 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2186 )) 2187 2188 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2189 2190 info.extend([ 2191 "\n## Portfolio distribution by companies\n" 2192 "\n| Company | Percent | Current cost |\n", 2193 aSepLine, 2194 ]) 2195 2196 for company in view["analytics"]["distrByCompanies"].keys(): 2197 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2198 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2199 "{}{}".format( 2200 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2201 company, 2202 ), 2203 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2204 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2205 )) 2206 2207 info.extend([ 2208 "\n## Portfolio distribution by sectors\n" 2209 "\n| Sector | Percent | Current cost |\n", 2210 aSepLine, 2211 ]) 2212 2213 for sector in view["analytics"]["distrBySectors"].keys(): 2214 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2215 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2216 sector, 2217 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2218 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2219 )) 2220 2221 info.extend([ 2222 "\n## Portfolio distribution by currencies\n" 2223 "\n| Instruments currencies | Percent | Current cost |\n", 2224 aSepLine, 2225 ]) 2226 2227 for curr in view["analytics"]["distrByCurrencies"].keys(): 2228 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2229 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2230 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2231 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2232 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2233 )) 2234 2235 info.extend([ 2236 "\n## Portfolio distribution by countries\n" 2237 "\n| Assets by country | Percent | Current cost |\n", 2238 aSepLine, 2239 ]) 2240 2241 for country in view["analytics"]["distrByCountries"].keys(): 2242 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2243 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2244 country, 2245 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2246 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2247 )) 2248 2249 if details in ["full", "calendar"]: 2250 # -- Show bonds payment calendar section: 2251 if view["stat"]["Bonds"]: 2252 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2253 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2254 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2255 2256 else: 2257 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2258 2259 infoText = "".join(info) 2260 2261 uLogger.info(infoText) 2262 2263 if details == "full" and self.overviewFile: 2264 filename = self.overviewFile 2265 2266 elif details == "digest" and self.overviewDigestFile: 2267 filename = self.overviewDigestFile 2268 2269 elif details == "positions" and self.overviewPositionsFile: 2270 filename = self.overviewPositionsFile 2271 2272 elif details == "orders" and self.overviewOrdersFile: 2273 filename = self.overviewOrdersFile 2274 2275 elif details == "analytics" and self.overviewAnalyticsFile: 2276 filename = self.overviewAnalyticsFile 2277 2278 elif details == "calendar" and self.overviewBondsCalendarFile: 2279 filename = self.overviewBondsCalendarFile 2280 2281 else: 2282 filename = "" 2283 2284 if filename: 2285 with open(filename, "w", encoding="UTF-8") as fH: 2286 fH.write(infoText) 2287 2288 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2289 2290 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2292 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2293 """ 2294 Returns history operations between two given dates for current `accountId`. 2295 If `reportFile` string is not empty then also save human-readable report. 2296 Shows some statistical data of closed positions. 2297 2298 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2299 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2300 :param show: if `True` then also prints all records to the console. 2301 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2302 :return: original list of dictionaries with history of deals records from API ("operations" key): 2303 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2304 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2305 """ 2306 if self.accountId is None or not self.accountId: 2307 uLogger.error("Variable `accountId` must be defined for using this method!") 2308 raise Exception("Account ID required") 2309 2310 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2311 2312 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2313 2314 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2315 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2316 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2317 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2318 customStat = {} # custom statistics in additional to responseJSON 2319 2320 # --- output report in human-readable format: 2321 if show or self.reportFile: 2322 splitLine1 = "| | | | | |\n" # Summary section 2323 splitLine2 = "| | | | | | | | |\n" # Operations section 2324 nextDay = "" 2325 2326 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2327 2328 if len(ops) > 0: 2329 customStat = { 2330 "opsCount": 0, # total operations count 2331 "buyCount": 0, # buy operations 2332 "sellCount": 0, # sell operations 2333 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2334 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2335 "payIn": {"rub": 0.}, # Deposit brokerage account 2336 "payOut": {"rub": 0.}, # Withdrawals 2337 "divs": {"rub": 0.}, # Dividends income 2338 "coupons": {"rub": 0.}, # Coupon's income 2339 "brokerCom": {"rub": 0.}, # Service commissions 2340 "serviceCom": {"rub": 0.}, # Service commissions 2341 "marginCom": {"rub": 0.}, # Margin commissions 2342 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2343 } 2344 2345 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2346 for item in ops: 2347 if item["state"] == "OPERATION_STATE_EXECUTED": 2348 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2349 2350 # count buy operations: 2351 if "_BUY" in item["operationType"]: 2352 customStat["buyCount"] += 1 2353 2354 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2355 customStat["buyTotal"][item["payment"]["currency"]] += payment 2356 2357 else: 2358 customStat["buyTotal"][item["payment"]["currency"]] = payment 2359 2360 # count sell operations: 2361 elif "_SELL" in item["operationType"]: 2362 customStat["sellCount"] += 1 2363 2364 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2365 customStat["sellTotal"][item["payment"]["currency"]] += payment 2366 2367 else: 2368 customStat["sellTotal"][item["payment"]["currency"]] = payment 2369 2370 # count incoming operations: 2371 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2372 if item["payment"]["currency"] in customStat["payIn"].keys(): 2373 customStat["payIn"][item["payment"]["currency"]] += payment 2374 2375 else: 2376 customStat["payIn"][item["payment"]["currency"]] = payment 2377 2378 # count withdrawals operations: 2379 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2380 if item["payment"]["currency"] in customStat["payOut"].keys(): 2381 customStat["payOut"][item["payment"]["currency"]] += payment 2382 2383 else: 2384 customStat["payOut"][item["payment"]["currency"]] = payment 2385 2386 # count dividends income: 2387 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2388 if item["payment"]["currency"] in customStat["divs"].keys(): 2389 customStat["divs"][item["payment"]["currency"]] += payment 2390 2391 else: 2392 customStat["divs"][item["payment"]["currency"]] = payment 2393 2394 # count coupon's income: 2395 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2396 if item["payment"]["currency"] in customStat["coupons"].keys(): 2397 customStat["coupons"][item["payment"]["currency"]] += payment 2398 2399 else: 2400 customStat["coupons"][item["payment"]["currency"]] = payment 2401 2402 # count broker commissions: 2403 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2404 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2405 customStat["brokerCom"][item["payment"]["currency"]] += payment 2406 2407 else: 2408 customStat["brokerCom"][item["payment"]["currency"]] = payment 2409 2410 # count service commissions: 2411 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2412 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2413 customStat["serviceCom"][item["payment"]["currency"]] += payment 2414 2415 else: 2416 customStat["serviceCom"][item["payment"]["currency"]] = payment 2417 2418 # count margin commissions: 2419 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2420 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2421 customStat["marginCom"][item["payment"]["currency"]] += payment 2422 2423 else: 2424 customStat["marginCom"][item["payment"]["currency"]] = payment 2425 2426 # count withholding taxes: 2427 elif "_TAX" in item["operationType"]: 2428 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2429 customStat["allTaxes"][item["payment"]["currency"]] += payment 2430 2431 else: 2432 customStat["allTaxes"][item["payment"]["currency"]] = payment 2433 2434 else: 2435 continue 2436 2437 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2438 2439 # --- view "Actions" lines: 2440 info.extend([ 2441 "| Report sections | | | | |\n", 2442 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2443 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2444 "| | Buy: {:<22} | {:<28} | | |\n".format( 2445 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2446 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2447 ), 2448 "| | Sell: {:<21} | {:<28} | | |\n".format( 2449 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2450 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2451 ), 2452 ]) 2453 2454 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2455 for key in opsKeys: 2456 if key == "rub": 2457 continue 2458 2459 info.extend([ 2460 "| | | {:<28} | | |\n".format( 2461 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2462 ), 2463 "| | | {:<28} | | |\n".format( 2464 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2465 ), 2466 ]) 2467 2468 info.append(splitLine1) 2469 2470 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2471 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2472 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2473 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2474 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2475 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2476 ) 2477 2478 # --- view "Payments" lines: 2479 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2480 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2481 2482 for key in paymentsKeys: 2483 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2484 2485 info.append(splitLine1) 2486 2487 # --- view "Commissions and taxes" lines: 2488 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2489 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2490 2491 for key in comKeys: 2492 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2493 2494 info.append(splitLine1) 2495 2496 info.extend([ 2497 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2498 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2499 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2500 ]) 2501 2502 else: 2503 info.append("Broker returned no operations during this period\n") 2504 2505 # --- view "Operations" section: 2506 for item in ops: 2507 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2508 continue 2509 2510 else: 2511 self.figi = item["figi"] if item["figi"] else "" 2512 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2513 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2514 2515 # group of deals during one day: 2516 if nextDay and item["date"].split("T")[0] != nextDay: 2517 info.append(splitLine2) 2518 nextDay = "" 2519 2520 else: 2521 nextDay = item["date"].split("T")[0] # saving current day for splitting 2522 2523 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2524 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2525 self.figi if self.figi else "—", 2526 instrument["ticker"] if instrument else "—", 2527 instrument["type"] if instrument else "—", 2528 item["quantity"] if int(item["quantity"]) > 0 else "—", 2529 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2530 TKS_OPERATION_STATES[item["state"]], 2531 TKS_OPERATION_TYPES[item["operationType"]], 2532 )) 2533 2534 infoText = "".join(info) 2535 2536 if show: 2537 if self.moreDebug: 2538 uLogger.debug("Records about history of a client's operations successfully received") 2539 2540 uLogger.info(infoText) 2541 2542 if self.reportFile: 2543 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2544 fH.write(infoText) 2545 2546 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2547 2548 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2550 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2551 """ 2552 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2553 2554 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2555 Warning! Broker server used ISO UTC time by default. 2556 2557 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2558 Also, `historyFile` used to update history with `onlyMissing` parameter. 2559 2560 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2561 2562 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2563 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2564 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2565 `"hour"`, `"day"`. Default: `"hour"`. 2566 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2567 False by default. Warning! History appends only from last candle to current time 2568 with always update last candle! 2569 :param csvSep: separator if csv-file is used, `,` by default. 2570 :param show: if `True` then also prints Pandas DataFrame to the console. 2571 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2572 `["date", "time", "open", "high", "low", "close", "volume"]`. 2573 """ 2574 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2575 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2576 history = None # empty pandas object for history 2577 2578 if interval not in TKS_CANDLE_INTERVALS.keys(): 2579 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2580 raise Exception("Incorrect value") 2581 2582 if not (self.ticker or self.figi): 2583 uLogger.error("Ticker or FIGI must be defined!") 2584 raise Exception("Ticker or FIGI required") 2585 2586 if self.ticker and not self.figi: 2587 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2588 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2589 2590 if self.figi and not self.ticker: 2591 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2592 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2593 2594 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2595 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2596 if interval.lower() != "day": 2597 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2598 2599 delta = dtEnd - dtStart # current UTC time minus last time in file 2600 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2601 2602 # calculate history length in candles: 2603 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2604 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2605 length += 1 # to avoid fraction time 2606 2607 # calculate data blocks count: 2608 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2609 2610 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2611 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2612 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2613 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2614 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2615 2616 tempOld = None # pandas object for old history, if --only-missing key present 2617 lastTime = None # datetime object of last old candle in file 2618 2619 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2620 uLogger.debug("--only-missing key present, add only last missing candles...") 2621 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2622 2623 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2624 2625 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2626 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2627 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2628 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2629 2630 # get last datetime object from last string in file or minus 1 delta if file is empty: 2631 if len(tempOld) > 0: 2632 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2633 2634 else: 2635 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2636 2637 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2638 2639 responseJSONs = [] # raw history blocks of data 2640 2641 blockEnd = dtEnd 2642 for item in range(blocks): 2643 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2644 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2645 2646 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2647 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2648 )) 2649 2650 if blockStart == blockEnd: 2651 uLogger.debug("Skipped this zero-length block...") 2652 2653 else: 2654 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2655 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2656 self.body = str({ 2657 "figi": self.figi, 2658 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2659 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2660 "interval": TKS_CANDLE_INTERVALS[interval][0] 2661 }) 2662 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2663 2664 if "code" in responseJSON.keys(): 2665 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2666 2667 else: 2668 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2669 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2670 2671 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2672 2673 blockEnd = blockStart 2674 2675 printCount = len(responseJSONs) # candles to show in console 2676 if responseJSONs: 2677 tempHistory = pd.DataFrame( 2678 data={ 2679 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2680 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2681 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2682 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2683 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2684 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2685 "volume": [int(item["volume"]) for item in responseJSONs], 2686 }, 2687 index=range(len(responseJSONs)), 2688 columns=["date", "time", "open", "high", "low", "close", "volume"], 2689 ) 2690 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2691 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2692 2693 # append only newest candles to old history if --only-missing key present: 2694 if onlyMissing and tempOld is not None and lastTime is not None: 2695 index = 0 # find start index in tempHistory data: 2696 2697 for i, item in tempHistory.iterrows(): 2698 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2699 2700 if curTime == lastTime: 2701 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2702 index = i 2703 printCount = index + 1 2704 break 2705 2706 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2707 2708 else: 2709 history = tempHistory # if no `--only-missing` key then load full data from server 2710 2711 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2712 2713 if history is not None and not history.empty: 2714 if show: 2715 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2716 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2717 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2718 )) 2719 2720 else: 2721 uLogger.warning("Received an empty candles history!") 2722 2723 if self.historyFile is not None: 2724 if history is not None and not history.empty: 2725 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2726 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2727 2728 else: 2729 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2730 2731 else: 2732 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2733 2734 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2736 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2737 """ 2738 Load candles history from csv-file and return Pandas DataFrame object. 2739 2740 See also: `History()` and `ShowHistoryChart()` methods. 2741 2742 :param filePath: path to csv-file to open. 2743 """ 2744 loadedHistory = None # init candles data object 2745 2746 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2747 2748 if os.path.exists(filePath): 2749 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2750 2751 tfStr = self.priceModel.FormattedDelta( 2752 self.priceModel.timeframe, 2753 "{days} days {hours}h {minutes}m {seconds}s", 2754 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2755 self.priceModel.timeframe, 2756 "{hours}h {minutes}m {seconds}s", 2757 ) 2758 2759 if loadedHistory is not None and not loadedHistory.empty: 2760 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2761 len(loadedHistory), 2762 tfStr, 2763 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2764 ) 2765 2766 else: 2767 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2768 2769 else: 2770 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2771 2772 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2774 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2775 """ 2776 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2777 2778 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2779 Default: `index.html` (both for interact and non-interact candlesticks chart). 2780 2781 See also: `History()` and `LoadHistory()` methods. 2782 2783 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2784 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2785 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2786 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2787 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2788 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2789 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2790 """ 2791 if isinstance(candles, str): 2792 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2793 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2794 2795 elif isinstance(candles, pd.DataFrame): 2796 self.priceModel.prices = candles # set candles chain from variable 2797 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2798 2799 if "datetime" not in candles.columns: 2800 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2801 2802 else: 2803 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2804 raise Exception("Incorrect value") 2805 2806 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2807 2808 if interact: 2809 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2810 2811 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2812 2813 else: 2814 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2815 2816 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2817 2818 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2820 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2821 """ 2822 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2823 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2824 2825 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2826 2827 :param operation: string "Buy" or "Sell". 2828 :param lots: volume, integer count of lots >= 1. 2829 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2830 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2831 :param expDate: string "Undefined" by default or local date in future, 2832 it is a string with format `%Y-%m-%d %H:%M:%S`. 2833 :return: JSON with response from broker server. 2834 """ 2835 if self.accountId is None or not self.accountId: 2836 uLogger.error("Variable `accountId` must be defined for using this method!") 2837 raise Exception("Account ID required") 2838 2839 if operation is None or not operation or operation not in ("Buy", "Sell"): 2840 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2841 raise Exception("Incorrect value") 2842 2843 if lots is None or lots < 1: 2844 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2845 lots = 1 2846 2847 if tp is None or tp < 0: 2848 tp = 0 2849 2850 if sl is None or sl < 0: 2851 sl = 0 2852 2853 if expDate is None or not expDate: 2854 expDate = "Undefined" 2855 2856 if not (self.ticker or self.figi): 2857 uLogger.error("Ticker or FIGI must be defined!") 2858 raise Exception("Ticker or FIGI required") 2859 2860 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2861 self.ticker = instrument["ticker"] 2862 self.figi = instrument["figi"] 2863 2864 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2865 2866 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2867 self.body = str({ 2868 "figi": self.figi, 2869 "quantity": str(lots), 2870 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2871 "accountId": str(self.accountId), 2872 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2873 }) 2874 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2875 2876 if "orderId" in response.keys(): 2877 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2878 operation, response["orderId"], 2879 self.ticker, self.figi, lots, 2880 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2881 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2882 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2883 )) 2884 2885 if tp > 0: 2886 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2887 2888 if sl > 0: 2889 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2890 2891 else: 2892 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2893 2894 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2896 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2897 """ 2898 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2899 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2900 2901 See also: `Order()` and `Trade()` docstrings. 2902 2903 :param lots: volume, integer count of lots >= 1. 2904 :param tp: float > 0, take profit price of stop-order. 2905 :param sl: float > 0, stop loss price of stop-order. 2906 :param expDate: it's a local date in future. 2907 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2908 :return: JSON with response from broker server. 2909 """ 2910 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2912 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2913 """ 2914 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2915 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2916 2917 See also: `Order()` and `Trade()` docstrings. 2918 2919 :param lots: volume, integer count of lots >= 1. 2920 :param tp: float > 0, take profit price of stop-order. 2921 :param sl: float > 0, stop loss price of stop-order. 2922 :param expDate: it's a local date in the future. 2923 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2924 :return: JSON with response from broker server. 2925 """ 2926 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2928 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 2929 """ 2930 Close position of given instruments. 2931 2932 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 2933 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2934 This avoids unnecessary downloading data from the server. 2935 """ 2936 if instruments is None or not instruments: 2937 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 if isinstance(instruments, str): 2941 instruments = [instruments] 2942 2943 uniqueInstruments = self.GetUniqueFIGIs(instruments) 2944 if uniqueInstruments: 2945 if portfolio is None or not portfolio: 2946 portfolio = self.Overview(show=False) 2947 2948 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2949 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 2950 2951 for self.figi in uniqueInstruments: 2952 if self.figi not in allOpened: 2953 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 2954 continue 2955 2956 # search open trade info about instrument by ticker: 2957 instrument = {} 2958 for iType in TKS_INSTRUMENTS: 2959 if instrument: 2960 break 2961 2962 for item in portfolio["stat"][iType]: 2963 if item["figi"] == self.figi: 2964 instrument = item 2965 break 2966 2967 if instrument: 2968 self.ticker = instrument["ticker"] 2969 self.figi = instrument["figi"] 2970 2971 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 2972 self.ticker, 2973 self.figi, 2974 int(instrument["volume"]), 2975 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 2976 )) 2977 2978 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 2979 2980 if tradeLots > 0: 2981 if instrument["blocked"] > 0: 2982 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 2983 instrument["blocked"], 2984 self.ticker, 2985 tradeLots, 2986 )) 2987 2988 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 2989 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 2990 2991 else: 2992 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
2994 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 2995 """ 2996 Close all positions of given instruments with defined type. 2997 2998 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 2999 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3000 This avoids unnecessary downloading data from the server. 3001 """ 3002 if iType not in TKS_INSTRUMENTS: 3003 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3004 3005 else: 3006 if portfolio is None or not portfolio: 3007 portfolio = self.Overview(show=False) 3008 3009 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3010 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3011 3012 if tickers and portfolio: 3013 self.CloseTrades(tickers, portfolio) 3014 3015 else: 3016 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3018 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3019 """ 3020 Universal method to create market or limit orders with all available parameters for current `accountId`. 3021 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3022 3023 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3024 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3025 3026 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3027 then broker immediately open market order as you can do simple --buy or --sell operations! 3028 3029 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3030 When current price will go up or down to target price value then broker opens a limit order. 3031 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3032 3033 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3034 3035 :param operation: string "Buy" or "Sell". 3036 :param orderType: string "Limit" or "Stop". 3037 :param lots: volume, integer count of lots >= 1. 3038 :param targetPrice: target price > 0. This is open trade price for limit order. 3039 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3040 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3041 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3042 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3043 Stop loss order always executed by market price. 3044 :param expDate: string "Undefined" by default or local date in future. 3045 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3046 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3047 A limit order has no expiration date, it lasts until the end of the trading day. 3048 :return: JSON with response from broker server. 3049 """ 3050 if self.accountId is None or not self.accountId: 3051 uLogger.error("Variable `accountId` must be defined for using this method!") 3052 raise Exception("Account ID required") 3053 3054 if operation is None or not operation or operation not in ("Buy", "Sell"): 3055 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3056 raise Exception("Incorrect value") 3057 3058 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3059 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3060 raise Exception("Incorrect value") 3061 3062 if lots is None or lots < 1: 3063 uLogger.error("You must define trade volume > 0: integer count of lots!") 3064 raise Exception("Incorrect value") 3065 3066 if targetPrice is None or targetPrice <= 0: 3067 uLogger.error("Target price for limit-order must be greater than 0!") 3068 raise Exception("Incorrect value") 3069 3070 if limitPrice is None or limitPrice <= 0: 3071 limitPrice = targetPrice 3072 3073 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3074 stopType = "Limit" 3075 3076 if expDate is None or not expDate: 3077 expDate = "Undefined" 3078 3079 if not (self.ticker or self.figi): 3080 uLogger.error("Tocker or FIGI must be defined!") 3081 raise Exception("Ticker or FIGI required") 3082 3083 response = {} 3084 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3085 self.ticker = instrument["ticker"] 3086 self.figi = instrument["figi"] 3087 3088 if orderType == "Limit": 3089 uLogger.debug( 3090 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3091 self.ticker, self.figi, 3092 operation, lots, targetPrice, instrument["currency"], 3093 )) 3094 3095 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3096 self.body = str({ 3097 "figi": self.figi, 3098 "quantity": str(lots), 3099 "price": FloatToNano(targetPrice), 3100 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3101 "accountId": str(self.accountId), 3102 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3103 }) 3104 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3105 3106 if "orderId" in response.keys(): 3107 uLogger.info( 3108 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3109 response["orderId"], 3110 self.ticker, self.figi, 3111 operation, lots, targetPrice, instrument["currency"], 3112 )) 3113 3114 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3115 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3116 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3117 targetPrice, instrument["currency"], 3118 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3119 )) 3120 3121 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3122 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3123 targetPrice, instrument["currency"], 3124 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3125 )) 3126 3127 else: 3128 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3129 3130 if orderType == "Stop": 3131 uLogger.debug( 3132 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3133 self.ticker, self.figi, 3134 operation, lots, 3135 targetPrice, instrument["currency"], 3136 limitPrice, instrument["currency"], 3137 stopType, expDate, 3138 )) 3139 3140 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3141 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3142 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3143 3144 body = { 3145 "figi": self.figi, 3146 "quantity": str(lots), 3147 "price": FloatToNano(limitPrice), 3148 "stopPrice": FloatToNano(targetPrice), 3149 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3150 "accountId": str(self.accountId), 3151 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3152 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3153 } 3154 3155 if expDateUTC: 3156 body["expireDate"] = expDateUTC 3157 3158 self.body = str(body) 3159 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3160 3161 if "stopOrderId" in response.keys(): 3162 uLogger.info( 3163 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3164 response["stopOrderId"], 3165 self.ticker, self.figi, 3166 operation, lots, 3167 targetPrice, instrument["currency"], 3168 limitPrice, instrument["currency"], 3169 TKS_STOP_ORDER_TYPES[stopOrderType], 3170 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3171 )) 3172 3173 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3174 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3175 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3176 targetPrice, instrument["currency"], 3177 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3178 )) 3179 3180 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3181 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3182 targetPrice, instrument["currency"], 3183 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3184 )) 3185 3186 else: 3187 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3188 3189 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3191 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3192 """ 3193 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3194 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3195 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3196 See also: `Order()` docstring. 3197 3198 :param lots: volume, integer count of lots >= 1. 3199 :param targetPrice: target price > 0. This is open trade price for limit order. 3200 :return: JSON with response from broker server. 3201 """ 3202 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3204 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3205 """ 3206 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3207 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3208 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3209 target price value then broker opens a limit order. See also: `Order()` docstring. 3210 3211 :param lots: volume, integer count of lots >= 1. 3212 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3213 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3214 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3215 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3216 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3217 :param expDate: string "Undefined" by default or local date in future. 3218 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3219 This date is converting to UTC format for server. 3220 :return: JSON with response from broker server. 3221 """ 3222 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3224 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3225 """ 3226 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3227 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3228 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3229 See also: `Order()` docstring. 3230 3231 :param lots: volume, integer count of lots >= 1. 3232 :param targetPrice: target price > 0. This is open trade price for limit order. 3233 :return: JSON with response from broker server. 3234 """ 3235 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3237 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3238 """ 3239 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3240 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3241 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3242 target price value then broker opens a limit order. See also: `Order()` docstring. 3243 3244 :param lots: volume, integer count of lots >= 1. 3245 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3246 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3247 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3248 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3249 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3250 :param expDate: string "Undefined" by default or local date in future. 3251 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3252 This date is converting to UTC format for server. 3253 :return: JSON with response from broker server. 3254 """ 3255 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3257 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3258 """ 3259 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3260 3261 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3262 :param allOrdersIDs: pre-received lists of all active pending orders. 3263 This avoids unnecessary downloading data from the server. 3264 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3265 """ 3266 if self.accountId is None or not self.accountId: 3267 uLogger.error("Variable `accountId` must be defined for using this method!") 3268 raise Exception("Account ID required") 3269 3270 if orderIDs: 3271 if allOrdersIDs is None or not allOrdersIDs: 3272 rawOrders = self.RequestPendingOrders() 3273 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3274 3275 if allStopOrdersIDs is None or not allStopOrdersIDs: 3276 rawStopOrders = self.RequestStopOrders() 3277 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3278 3279 for orderID in orderIDs: 3280 idInPendingOrders = orderID in allOrdersIDs 3281 idInStopOrders = orderID in allStopOrdersIDs 3282 3283 if not (idInPendingOrders or idInStopOrders): 3284 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3285 continue 3286 3287 else: 3288 if idInPendingOrders: 3289 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3290 3291 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3292 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3293 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3294 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3295 3296 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3297 if self.moreDebug: 3298 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3299 3300 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3301 3302 else: 3303 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3304 3305 elif idInStopOrders: 3306 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3307 3308 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3309 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3310 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3311 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3312 3313 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3314 if self.moreDebug: 3315 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3316 3317 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3318 3319 else: 3320 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3321 3322 else: 3323 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3325 def CloseAllOrders(self) -> None: 3326 """ 3327 Gets a list of open pending and stop orders and cancel it all. 3328 """ 3329 rawOrders = self.RequestPendingOrders() 3330 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3331 lenOrders = len(allOrdersIDs) 3332 3333 rawStopOrders = self.RequestStopOrders() 3334 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3335 lenSOrders = len(allStopOrdersIDs) 3336 3337 if lenOrders > 0 or lenSOrders > 0: 3338 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3339 3340 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3341 3342 else: 3343 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3345 def CloseAll(self, *args) -> None: 3346 """ 3347 Close all available (not blocked) opened trades and orders. 3348 3349 Also, you can select one or more keywords case-insensitive: 3350 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3351 3352 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3353 """ 3354 overview = self.Overview(show=False) # get all open trades info 3355 3356 if len(args) == 0: 3357 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3358 self.CloseAllOrders() # close all pending and stop orders 3359 3360 for iType in TKS_INSTRUMENTS: 3361 if iType != "Currencies": 3362 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3363 3364 else: 3365 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3366 lowerArgs = [x.lower() for x in args] 3367 3368 if "orders" in lowerArgs: 3369 self.CloseAllOrders() # close all pending and stop orders 3370 3371 for iType in TKS_INSTRUMENTS: 3372 if iType.lower() in lowerArgs and iType != "Currencies": 3373 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3375 @staticmethod 3376 def ParseOrderParameters(operation, **inputParameters): 3377 """ 3378 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3379 3380 :param operation: string "Buy" or "Sell". 3381 :param inputParameters: this is dict of strings that looks like this 3382 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3383 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3384 "prices" key: one or more prices to open limit-orders 3385 Counts of values in lots and prices lists must be equals! 3386 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3387 """ 3388 # TODO: update order grid work with api v2 3389 pass 3390 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3391 # 3392 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3393 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3394 # raise Exception("Incorrect value") 3395 # 3396 # if "l" in inputParameters.keys(): 3397 # inputParameters["lots"] = inputParameters.pop("l") 3398 # 3399 # if "p" in inputParameters.keys(): 3400 # inputParameters["prices"] = inputParameters.pop("p") 3401 # 3402 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3403 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3404 # raise Exception("Incorrect value") 3405 # 3406 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3407 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3408 # 3409 # if len(lots) != len(prices): 3410 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3411 # raise Exception("Incorrect value") 3412 # 3413 # uLogger.debug("Extracted parameters for orders:") 3414 # uLogger.debug("lots = {}".format(lots)) 3415 # uLogger.debug("prices = {}".format(prices)) 3416 # 3417 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3418 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3419 # uLogger.debug("Order parameters: {}".format(result)) 3420 # 3421 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3423 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3424 """ 3425 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3426 3427 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3428 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3429 """ 3430 result = False 3431 msg = "Instrument not defined!" 3432 3433 if portfolio is None or not portfolio: 3434 portfolio = self.Overview(show=False) 3435 3436 if self.ticker: 3437 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3438 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3439 3440 for iType in TKS_INSTRUMENTS: 3441 for instrument in portfolio["stat"][iType]: 3442 if instrument["ticker"] == self.ticker: 3443 result = True 3444 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3445 break 3446 3447 elif self.figi: 3448 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3449 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3450 3451 for iType in TKS_INSTRUMENTS: 3452 for instrument in portfolio["stat"][iType]: 3453 if instrument["figi"] == self.figi: 3454 result = True 3455 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3456 break 3457 3458 else: 3459 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3460 3461 uLogger.debug(msg) 3462 3463 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3465 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3466 """ 3467 Returns instrument from the user's portfolio if it presents there. 3468 Instrument must be defined by `ticker` (highly priority) or `figi`. 3469 3470 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3471 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3472 """ 3473 result = None 3474 msg = "Instrument not defined!" 3475 3476 if portfolio is None or not portfolio: 3477 portfolio = self.Overview(show=False) 3478 3479 if self.ticker: 3480 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker)) 3481 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3482 3483 for iType in TKS_INSTRUMENTS: 3484 for instrument in portfolio["stat"][iType]: 3485 if instrument["ticker"] == self.ticker: 3486 result = instrument 3487 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3488 break 3489 3490 elif self.figi: 3491 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3492 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3493 3494 for iType in TKS_INSTRUMENTS: 3495 for instrument in portfolio["stat"][iType]: 3496 if instrument["figi"] == self.figi: 3497 result = instrument 3498 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3499 break 3500 3501 else: 3502 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3503 3504 uLogger.debug(msg) 3505 3506 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3508 def RequestLimits(self) -> dict: 3509 """ 3510 Method for obtaining the available funds for withdrawal for current `accountId`. 3511 3512 See also: 3513 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3514 - `OverviewLimits()` method 3515 3516 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3517 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3518 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3519 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3520 """ 3521 if self.accountId is None or not self.accountId: 3522 uLogger.error("Variable `accountId` must be defined for using this method!") 3523 raise Exception("Account ID required") 3524 3525 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3526 3527 self.body = str({"accountId": self.accountId}) 3528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3529 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3530 3531 if self.moreDebug: 3532 uLogger.debug("Records about available funds for withdrawal successfully received") 3533 3534 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3536 def OverviewLimits(self, show: bool = False) -> dict: 3537 """ 3538 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3539 3540 See also: `RequestLimits()`. 3541 3542 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3543 :return: dict with raw parsed data from server and some calculated statistics about it. 3544 """ 3545 if self.accountId is None or not self.accountId: 3546 uLogger.error("Variable `accountId` must be defined for using this method!") 3547 raise Exception("Account ID required") 3548 3549 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3550 3551 view = { 3552 "rawLimits": rawLimits, 3553 "limits": { # parsed data for every currency: 3554 "money": { # this is an array of portfolio currency positions 3555 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3556 }, 3557 "blocked": { # this is an array of blocked currency 3558 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3559 }, 3560 "blockedGuarantee": { # this is locked money under collateral for futures 3561 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3562 }, 3563 }, 3564 } 3565 3566 # --- Prepare text table with limits in human-readable format: 3567 if show: 3568 info = [ 3569 "# Withdrawal limits\n\n", 3570 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3571 "* **Account ID:** [{}]\n".format(self.accountId), 3572 ] 3573 3574 if view["limits"]["money"]: 3575 info.extend([ 3576 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3577 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3578 ]) 3579 3580 else: 3581 info.append("\nNo withdrawal limits\n") 3582 3583 for curr in view["limits"]["money"].keys(): 3584 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3585 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3586 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3587 3588 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3589 "[{}]".format(curr), 3590 "{:.2f}".format(view["limits"]["money"][curr]), 3591 "{:.2f}".format(availableMoney), 3592 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3593 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3594 ) 3595 3596 if curr == "rub": 3597 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3598 3599 else: 3600 info.append(infoStr) 3601 3602 infoText = "".join(info) 3603 3604 uLogger.info(infoText) 3605 3606 if self.withdrawalLimitsFile: 3607 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3608 fH.write(infoText) 3609 3610 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3611 3612 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3614 def RequestAccounts(self) -> dict: 3615 """ 3616 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3617 3618 See also: 3619 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3620 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3621 - `OverviewUserInfo()` method 3622 3623 :return: dict with raw data from server that contains accounts info. Example of dict: 3624 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3625 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3626 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3627 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3628 """ 3629 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3630 3631 self.body = str({}) 3632 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3633 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3634 3635 if self.moreDebug: 3636 uLogger.debug("Records about available accounts successfully received") 3637 3638 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3640 def RequestUserInfo(self) -> dict: 3641 """ 3642 Method for requesting common user's information. 3643 3644 See also: 3645 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3646 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3647 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3648 - `OverviewUserInfo()` method 3649 3650 :return: dict with raw data from server that contains user's information. Example of dict: 3651 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3652 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3653 """ 3654 uLogger.debug("Requesting common user's information. Wait, please...") 3655 3656 self.body = str({}) 3657 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3658 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3659 3660 if self.moreDebug: 3661 uLogger.debug("Records about current user successfully received") 3662 3663 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3665 def RequestMarginStatus(self, accountId: str = None) -> dict: 3666 """ 3667 Method for requesting margin calculation for defined account ID. 3668 3669 See also: 3670 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3671 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3672 - `OverviewUserInfo()` method 3673 3674 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3675 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3676 Example of responses: 3677 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3678 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3679 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3680 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3681 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3682 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3683 """ 3684 if accountId is None or not accountId: 3685 if self.accountId is None or not self.accountId: 3686 uLogger.error("Variable `accountId` must be defined for using this method!") 3687 raise Exception("Account ID required") 3688 3689 else: 3690 accountId = self.accountId # use `self.accountId` (main ID) by default 3691 3692 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3693 3694 self.body = str({"accountId": accountId}) 3695 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3696 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3697 3698 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3699 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3700 rawMargin = {} 3701 3702 else: 3703 if self.moreDebug: 3704 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3705 3706 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3708 def RequestTariffLimits(self) -> dict: 3709 """ 3710 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3711 3712 See also: 3713 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3714 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3715 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3716 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3717 - `OverviewUserInfo()` method 3718 3719 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3720 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3721 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3722 """ 3723 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3724 3725 self.body = str({}) 3726 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3727 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3728 3729 if self.moreDebug: 3730 uLogger.debug("Records with limits of current tariff successfully received") 3731 3732 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3734 def RequestBondCoupons(self, iJSON: dict) -> dict: 3735 """ 3736 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3737 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3738 All dates are in UTC timezone. 3739 3740 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3741 Documentation: 3742 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3743 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3744 3745 See also: `ExtendBondsData()`. 3746 3747 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3748 If raw iJSON is not data of bond then server returns an error [400] with message: 3749 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3750 :return: dictionary with bond payment calendar. Response example 3751 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3752 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3753 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3754 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3755 """ 3756 if iJSON["figi"] is None or not iJSON["figi"]: 3757 uLogger.error("FIGI must be defined for using this method!") 3758 raise Exception("FIGI required") 3759 3760 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3761 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3762 3763 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3764 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3765 self.figi, 3766 startDate, 3767 endDate, 3768 )) 3769 3770 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3771 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3772 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3773 3774 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3775 uLogger.warning("Instrument type is not bond!") 3776 3777 else: 3778 if self.moreDebug: 3779 uLogger.debug("Records about bond payment calendar successfully received") 3780 3781 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3783 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3784 """ 3785 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3786 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3787 coupon yields, current yields and some statistics etc. 3788 3789 WARNING! This is too long operation if a lot of bonds requested from broker server. 3790 3791 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3792 3793 :param instruments: list of strings with tickers or FIGIs. 3794 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3795 for further used by data scientists or stock analytics. 3796 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3797 In XLSX-file and Pandas DataFrame fields mean: 3798 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3799 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3800 """ 3801 if instruments is None or not instruments: 3802 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3803 raise Exception("Ticker or FIGI required") 3804 3805 if isinstance(instruments, str): 3806 instruments = [instruments] 3807 3808 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3809 3810 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3811 3812 iCount = len(uniqueInstruments) 3813 tooLong = iCount >= 20 3814 if tooLong: 3815 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3816 3817 bonds = None 3818 for i, self.figi in enumerate(uniqueInstruments): 3819 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3820 3821 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3822 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3823 rawBond = self.SearchByFIGI(requestPrice=True) 3824 3825 # Widen raw data with UTC current time (iData["actualDateTime"]): 3826 actualDate = datetime.now(tzutc()) 3827 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3828 3829 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3830 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3831 3832 # Replace some values with human-readable: 3833 iData["nominalCurrency"] = iData["nominal"]["currency"] 3834 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3835 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3836 iData["aciCurrency"] = iData["aciValue"]["currency"] 3837 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3838 iData["issueSize"] = int(iData["issueSize"]) 3839 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3840 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3841 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3842 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3843 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3844 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3845 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3846 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3847 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3848 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3849 3850 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3851 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3852 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3853 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3854 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3855 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3856 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3857 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3858 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3859 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3860 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3861 3862 # Widen raw data with calendar data from `rawCalendar` values: 3863 calendarData = [] 3864 if "events" in iData["rawCalendar"].keys(): 3865 for item in iData["rawCalendar"]["events"]: 3866 calendarData.append({ 3867 "couponDate": item["couponDate"], 3868 "couponNumber": int(item["couponNumber"]), 3869 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3870 "payCurrency": item["payOneBond"]["currency"], 3871 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3872 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3873 "couponStartDate": item["couponStartDate"], 3874 "couponEndDate": item["couponEndDate"], 3875 "couponPeriod": item["couponPeriod"], 3876 }) 3877 3878 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3879 if "maturityDate" not in iData.keys(): 3880 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3881 3882 # Widen raw data with Coupon Rate. 3883 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3884 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3885 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3886 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3887 3888 # Widen raw data with Yield to Maturity (YTM) on current date. 3889 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3890 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3891 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3892 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3893 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3894 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3895 3896 iData["calendar"] = calendarData # adds calendar at the end 3897 3898 # Remove not used data: 3899 iData.pop("uid") 3900 iData.pop("positionUid") 3901 iData.pop("currentPrice") 3902 iData.pop("rawCalendar") 3903 3904 colNames = list(iData.keys()) 3905 if bonds is None: 3906 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3907 3908 else: 3909 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3910 3911 else: 3912 uLogger.warning("Instrument is not a bond!") 3913 3914 processed = round(100 * (i + 1) / iCount, 1) 3915 if tooLong and processed % 5 == 0: 3916 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3917 3918 else: 3919 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3920 3921 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3922 3923 # Saving bonds from Pandas DataFrame to XLSX sheet: 3924 if xlsx and self.bondsXLSXFile: 3925 with pd.ExcelWriter( 3926 path=self.bondsXLSXFile, 3927 date_format=TKS_DATE_FORMAT, 3928 datetime_format=TKS_DATE_TIME_FORMAT, 3929 mode="w", 3930 ) as writer: 3931 bonds.to_excel( 3932 writer, 3933 sheet_name="Extended bonds data", 3934 index=True, 3935 encoding="UTF-8", 3936 freeze_panes=(1, 1), 3937 ) # saving as XLSX-file with freeze first row and column as headers 3938 3939 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3940 3941 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3943 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3944 """ 3945 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 3946 3947 WARNING! This is too long operation if a lot of bonds requested from broker server. 3948 3949 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3950 3951 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 3952 extended information about bonds: main info, current prices, bond payment calendar, 3953 coupon yields, current yields and some statistics etc. 3954 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3955 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3956 for further used by data scientists or stock analytics. 3957 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3958 """ 3959 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3960 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3961 3962 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3963 3964 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3965 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3966 calendar = None 3967 for bond in extBonds.iterrows(): 3968 for item in bond[1]["calendar"]: 3969 cData = { 3970 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 3971 "couponDate": item["couponDate"], 3972 "figi": bond[1]["figi"], 3973 "ticker": bond[1]["ticker"], 3974 "name": bond[1]["name"], 3975 "couponNumber": item["couponNumber"], 3976 "payOneBond": item["payOneBond"], 3977 "payCurrency": item["payCurrency"], 3978 "couponType": item["couponType"], 3979 "couponPeriod": item["couponPeriod"], 3980 "fixDate": item["fixDate"], 3981 "couponStartDate": item["couponStartDate"], 3982 "couponEndDate": item["couponEndDate"], 3983 } 3984 3985 if calendar is None: 3986 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 3987 3988 else: 3989 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 3990 3991 if calendar is not None: 3992 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 3993 3994 # Saving calendar from Pandas DataFrame to XLSX sheet: 3995 if xlsx: 3996 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 3997 3998 with pd.ExcelWriter( 3999 path=xlsxCalendarFile, 4000 date_format=TKS_DATE_FORMAT, 4001 datetime_format=TKS_DATE_TIME_FORMAT, 4002 mode="w", 4003 ) as writer: 4004 humanReadable = calendar.copy(deep=True) 4005 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4006 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4007 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4008 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4009 humanReadable.columns = colNames # human-readable column names 4010 4011 humanReadable.to_excel( 4012 writer, 4013 sheet_name="Bond payments calendar", 4014 index=False, 4015 encoding="UTF-8", 4016 freeze_panes=(1, 2), 4017 ) # saving as XLSX-file with freeze first row and column as headers 4018 4019 del humanReadable # release df in memory 4020 4021 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4022 4023 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4025 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4026 """ 4027 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4028 Also, creates Markdown file with calendar data, `calendar.md` by default. 4029 4030 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4031 4032 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4033 extended information about bonds: main info, current prices, bond payment calendar, 4034 coupon yields, current yields and some statistics etc. 4035 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4036 :param show: if `True` then also printing bonds payment calendar to the console, 4037 otherwise save to file `calendarFile` only. `False` by default. 4038 :return: multilines text in Markdown format with bonds payment calendar as a table. 4039 """ 4040 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4041 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4042 4043 infoText = "# Bond payments calendar\n\n" 4044 4045 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4046 4047 if not (calendar is None or calendar.empty): 4048 splitLine = "| | | | | | | | | |\n" 4049 4050 info = [ 4051 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4052 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4053 ] 4054 4055 newMonth = False 4056 notOneBond = calendar["figi"].nunique() > 1 4057 for i, bond in enumerate(calendar.iterrows()): 4058 if newMonth and notOneBond: 4059 info.append(splitLine) 4060 4061 info.append( 4062 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4063 " √" if bond[1]["paid"] else " —", 4064 bond[1]["couponDate"].split("T")[0], 4065 bond[1]["figi"], 4066 bond[1]["ticker"], 4067 bond[1]["couponNumber"], 4068 "{} {}".format( 4069 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4070 bond[1]["payCurrency"], 4071 ), 4072 bond[1]["couponType"], 4073 bond[1]["couponPeriod"], 4074 bond[1]["fixDate"].split("T")[0], 4075 ) 4076 ) 4077 4078 if i < len(calendar.values) - 1: 4079 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4080 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4081 newMonth = False if curDate.month == nextDate.month else True 4082 4083 else: 4084 newMonth = False 4085 4086 infoText += "".join(info) 4087 4088 if show: 4089 uLogger.info("{}".format(infoText)) 4090 4091 if self.calendarFile is not None: 4092 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4093 fH.write(infoText) 4094 4095 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4096 4097 else: 4098 infoText += "No data\n" 4099 4100 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4102 def OverviewAccounts(self, show: bool = False) -> dict: 4103 """ 4104 Method for parsing and show simple table with all available user accounts. 4105 4106 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4107 4108 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4109 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4110 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4111 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4112 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4113 "closed": "—", "access": "Full access" }, ...}}` 4114 """ 4115 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4116 4117 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4118 accounts = { 4119 item["id"]: { 4120 "type": TKS_ACCOUNT_TYPES[item["type"]], 4121 "name": item["name"], 4122 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4123 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4124 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4125 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4126 } for item in rawAccounts["accounts"] 4127 } 4128 4129 # Raw and parsed data with some fields replaced in "stat" section: 4130 view = { 4131 "rawAccounts": rawAccounts, 4132 "stat": accounts, 4133 } 4134 4135 # --- Prepare simple text table with only accounts data in human-readable format: 4136 if show: 4137 info = [ 4138 "# User accounts\n\n", 4139 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4140 "| Account ID | Type | Status | Name |\n", 4141 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4142 ] 4143 4144 for account in view["stat"].keys(): 4145 info.extend([ 4146 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4147 account, 4148 view["stat"][account]["type"], 4149 view["stat"][account]["status"], 4150 view["stat"][account]["name"], 4151 ) 4152 ]) 4153 4154 infoText = "".join(info) 4155 4156 uLogger.info(infoText) 4157 4158 if self.userAccountsFile: 4159 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4160 fH.write(infoText) 4161 4162 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4163 4164 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4166 def OverviewUserInfo(self, show: bool = False) -> dict: 4167 """ 4168 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4169 4170 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4171 4172 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4173 :return: dict with raw parsed data from server and some calculated statistics about it. 4174 """ 4175 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4176 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4177 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4178 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4179 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4180 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4181 4182 # This is dict with parsed common user data: 4183 userInfo = { 4184 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4185 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4186 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4187 "tariff": rawUserInfo["tariff"], 4188 } 4189 4190 # This is an array of dict with parsed margin statuses for every account IDs: 4191 margins = {} 4192 for accountId in accounts.keys(): 4193 if rawMargins[accountId]: 4194 margins[accountId] = { 4195 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4196 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4197 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4198 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4199 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4200 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4201 } 4202 4203 else: 4204 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4205 4206 unary = {} # unary-connection limits 4207 for item in rawTariffLimits["unaryLimits"]: 4208 if item["limitPerMinute"] in unary.keys(): 4209 unary[item["limitPerMinute"]].extend(item["methods"]) 4210 4211 else: 4212 unary[item["limitPerMinute"]] = item["methods"] 4213 4214 stream = {} # stream-connection limits 4215 for item in rawTariffLimits["streamLimits"]: 4216 if item["limit"] in stream.keys(): 4217 stream[item["limit"]].extend(item["streams"]) 4218 4219 else: 4220 stream[item["limit"]] = item["streams"] 4221 4222 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4223 limits = { 4224 "unary": unary, 4225 "stream": stream, 4226 } 4227 4228 # Raw and parsed data as an output result: 4229 view = { 4230 "rawUserInfo": rawUserInfo, 4231 "rawAccounts": rawAccounts, 4232 "rawMargins": rawMargins, 4233 "rawTariffLimits": rawTariffLimits, 4234 "stat": { 4235 "userInfo": userInfo, 4236 "accounts": accounts, 4237 "margins": margins, 4238 "limits": limits, 4239 }, 4240 } 4241 4242 # --- Prepare text table with user information in human-readable format: 4243 if show: 4244 info = [ 4245 "# Full user information\n\n", 4246 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4247 "## Common information\n\n", 4248 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4249 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4250 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4251 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4252 "\n## User accounts\n\n", 4253 ] 4254 4255 for account in view["stat"]["accounts"].keys(): 4256 info.extend([ 4257 "### ID: [{}]\n\n".format(account), 4258 "| Parameters | Values |\n", 4259 "|----------------------|--------------------------------------------------------------|\n", 4260 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4261 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4262 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4263 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4264 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4265 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4266 ]) 4267 4268 if margins[account]: 4269 info.extend([ 4270 "| Margin status: | Enabled |\n", 4271 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4272 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4273 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4274 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4275 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4276 ]) 4277 4278 else: 4279 info.append("| Margin status: | Disabled |\n\n") 4280 4281 info.extend([ 4282 "\n## Current user tariff limits\n", 4283 "\nSee also:\n", 4284 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4285 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4286 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4287 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4288 "\n### Unary limits\n", 4289 ]) 4290 4291 if unary: 4292 for key, values in sorted(unary.items()): 4293 info.append("\n* Max requests per minute: {}\n".format(key)) 4294 4295 for value in values: 4296 info.append(" - {}\n".format(value)) 4297 4298 else: 4299 info.append("\nNot available\n") 4300 4301 info.append("\n### Stream limits\n") 4302 4303 if stream: 4304 for key, values in sorted(stream.items()): 4305 info.append("\n* Max stream connections: {}\n".format(key)) 4306 4307 for value in values: 4308 info.append(" - {}\n".format(value)) 4309 4310 else: 4311 info.append("\nNot available\n") 4312 4313 infoText = "".join(info) 4314 4315 uLogger.info(infoText) 4316 4317 if self.userInfoFile: 4318 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4319 fH.write(infoText) 4320 4321 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4322 4323 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4326class Args: 4327 """ 4328 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4329 """ 4330 def __init__(self, **kwargs): 4331 self.__dict__.update(kwargs) 4332 4333 def __getattr__(self, item): 4334 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4337def ParseArgs(): 4338 """This function get and parse command line keys.""" 4339 parser = ArgumentParser() # command-line string parser 4340 4341 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4342 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4343 4344 # --- options: 4345 4346 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4347 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4348 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4349 4350 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4351 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4352 4353 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4354 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4355 4356 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4357 4358 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4359 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4360 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4361 4362 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4363 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4364 4365 # --- commands: 4366 4367 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4368 4369 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4370 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4371 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4372 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4373 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4374 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4375 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4376 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4377 4378 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4379 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4380 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4381 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4382 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4383 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4384 4385 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4386 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4387 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4388 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4389 4390 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4391 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4392 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4393 4394 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4395 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4396 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4397 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4398 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4399 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4400 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4401 4402 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4403 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4404 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4405 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4406 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4407 4408 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4409 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4410 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4411 4412 cmdArgs = parser.parse_args() 4413 return cmdArgs
This function get and parse command line keys.
4416def Main(**kwargs): 4417 """ 4418 Main function for work with TKSBrokerAPI in the console. 4419 4420 See examples: 4421 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4422 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4423 """ 4424 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4425 4426 if args.debug_level: 4427 uLogger.level = 10 # always debug level by default 4428 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4429 4430 exitCode = 0 4431 start = datetime.now(tzutc()) 4432 uLogger.debug("=-" * 50) 4433 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4434 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4435 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4436 )) 4437 4438 # trying to calculate full current version: 4439 buildVersion = __version__ 4440 try: 4441 v = version("tksbrokerapi") 4442 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4443 4444 except Exception: 4445 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4446 4447 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4448 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4449 4450 try: 4451 if args.version: 4452 print("TKSBrokerAPI {}".format(buildVersion)) 4453 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4454 4455 else: 4456 # Init class for trading with Tinkoff Broker: 4457 trader = TinkoffBrokerServer( 4458 token=args.token, 4459 accountId=args.account_id, 4460 useCache=not args.no_cache, 4461 ) 4462 4463 # --- set some options: 4464 4465 if args.more: 4466 trader.moreDebug = True 4467 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4468 4469 if args.ticker: 4470 ticker = args.ticker.upper() # Tickers may be upper case only 4471 4472 if ticker in trader.aliasesKeys: 4473 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4474 4475 else: 4476 trader.ticker = ticker 4477 4478 if args.figi: 4479 trader.figi = args.figi.upper() # FIGIs may be upper case only 4480 4481 if args.depth is not None: 4482 trader.depth = args.depth 4483 4484 # --- do one command: 4485 4486 if args.list: 4487 if args.output is not None: 4488 trader.instrumentsFile = args.output 4489 4490 trader.ShowInstrumentsInfo(show=True) 4491 4492 elif args.list_xlsx: 4493 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4494 4495 elif args.bonds_xlsx is not None: 4496 if args.output is not None: 4497 trader.bondsXLSXFile = args.output 4498 4499 if len(args.bonds_xlsx) == 0: 4500 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4501 4502 else: 4503 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4504 4505 elif args.search: 4506 if args.output is not None: 4507 trader.searchResultsFile = args.output 4508 4509 trader.SearchInstruments(pattern=args.search[0], show=True) 4510 4511 elif args.info: 4512 if not (args.ticker or args.figi): 4513 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4514 raise Exception("Ticker or FIGI required") 4515 4516 if args.output is not None: 4517 trader.infoFile = args.output 4518 4519 if args.ticker: 4520 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4521 4522 else: 4523 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4524 4525 elif args.calendar is not None: 4526 if args.output is not None: 4527 trader.calendarFile = args.output 4528 4529 if len(args.calendar) == 0: 4530 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4531 4532 else: 4533 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4534 4535 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4536 4537 elif args.price: 4538 if not (args.ticker or args.figi): 4539 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4540 raise Exception("Ticker or FIGI required") 4541 4542 trader.GetCurrentPrices(show=True) 4543 4544 elif args.prices is not None: 4545 if args.output is not None: 4546 trader.pricesFile = args.output 4547 4548 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4549 4550 elif args.overview: 4551 if args.output is not None: 4552 trader.overviewFile = args.output 4553 4554 trader.Overview(show=True, details="full") 4555 4556 elif args.overview_digest: 4557 if args.output is not None: 4558 trader.overviewDigestFile = args.output 4559 4560 trader.Overview(show=True, details="digest") 4561 4562 elif args.overview_positions: 4563 if args.output is not None: 4564 trader.overviewPositionsFile = args.output 4565 4566 trader.Overview(show=True, details="positions") 4567 4568 elif args.overview_orders: 4569 if args.output is not None: 4570 trader.overviewOrdersFile = args.output 4571 4572 trader.Overview(show=True, details="orders") 4573 4574 elif args.overview_analytics: 4575 if args.output is not None: 4576 trader.overviewAnalyticsFile = args.output 4577 4578 trader.Overview(show=True, details="analytics") 4579 4580 elif args.overview_calendar: 4581 if args.output is not None: 4582 trader.overviewAnalyticsFile = args.output 4583 4584 trader.Overview(show=True, details="calendar") 4585 4586 elif args.deals is not None: 4587 if args.output is not None: 4588 trader.reportFile = args.output 4589 4590 if 0 <= len(args.deals) < 3: 4591 trader.Deals( 4592 start=args.deals[0] if len(args.deals) >= 1 else None, 4593 end=args.deals[1] if len(args.deals) == 2 else None, 4594 show=True, # Always show deals report in console 4595 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4596 ) 4597 4598 else: 4599 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4600 raise Exception("Incorrect value") 4601 4602 elif args.history is not None: 4603 if args.output is not None: 4604 trader.historyFile = args.output 4605 4606 if 0 <= len(args.history) < 3: 4607 dataReceived = trader.History( 4608 start=args.history[0] if len(args.history) >= 1 else None, 4609 end=args.history[1] if len(args.history) == 2 else None, 4610 interval="hour" if args.interval is None or not args.interval else args.interval, 4611 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4612 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4613 show=True, # shows all downloaded candles in console 4614 ) 4615 4616 if args.render_chart is not None and dataReceived is not None: 4617 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4618 4619 trader.ShowHistoryChart( 4620 candles=dataReceived, 4621 interact=iChart, 4622 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4623 ) 4624 4625 else: 4626 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4627 raise Exception("Incorrect value") 4628 4629 elif args.load_history is not None: 4630 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4631 4632 if args.render_chart is not None and histData is not None: 4633 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4634 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4635 4636 trader.ShowHistoryChart( 4637 candles=histData, 4638 interact=iChart, 4639 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4640 ) 4641 4642 elif args.trade is not None: 4643 if 1 <= len(args.trade) <= 5: 4644 trader.Trade( 4645 operation=args.trade[0], 4646 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4647 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4648 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4649 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4650 ) 4651 4652 else: 4653 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4654 4655 elif args.buy is not None: 4656 if 0 <= len(args.buy) <= 4: 4657 trader.Buy( 4658 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4659 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4660 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4661 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4662 ) 4663 4664 else: 4665 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4666 4667 elif args.sell is not None: 4668 if 0 <= len(args.sell) <= 4: 4669 trader.Sell( 4670 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4671 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4672 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4673 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4674 ) 4675 4676 else: 4677 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4678 4679 elif args.order: 4680 if 4 <= len(args.order) <= 7: 4681 trader.Order( 4682 operation=args.order[0], 4683 orderType=args.order[1], 4684 lots=int(args.order[2]), 4685 targetPrice=float(args.order[3]), 4686 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4687 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4688 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4689 ) 4690 4691 else: 4692 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4693 4694 elif args.buy_limit: 4695 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4696 4697 elif args.sell_limit: 4698 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4699 4700 elif args.buy_stop: 4701 if 2 <= len(args.buy_stop) <= 7: 4702 trader.BuyStop( 4703 lots=int(args.buy_stop[0]), 4704 targetPrice=float(args.buy_stop[1]), 4705 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4706 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4707 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4708 ) 4709 4710 else: 4711 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4712 4713 elif args.sell_stop: 4714 if 2 <= len(args.sell_stop) <= 7: 4715 trader.SellStop( 4716 lots=int(args.sell_stop[0]), 4717 targetPrice=float(args.sell_stop[1]), 4718 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4719 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4720 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4721 ) 4722 4723 else: 4724 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4725 4726 # elif args.buy_order_grid is not None: 4727 # # update order grid work with api v2 4728 # if len(args.buy_order_grid) == 2: 4729 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4730 # 4731 # for order in orderParams: 4732 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4733 # 4734 # else: 4735 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4736 # 4737 # elif args.sell_order_grid is not None: 4738 # # update order grid work with api v2 4739 # if len(args.sell_order_grid) >= 2: 4740 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4741 # 4742 # for order in orderParams: 4743 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4744 # 4745 # else: 4746 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4747 4748 elif args.close_order is not None: 4749 trader.CloseOrders(args.close_order) # close only one order 4750 4751 elif args.close_orders is not None: 4752 trader.CloseOrders(args.close_orders) # close list of orders 4753 4754 elif args.close_trade: 4755 if not (args.ticker or args.figi): 4756 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4757 raise Exception("Ticker or FIGI required") 4758 4759 if args.ticker: 4760 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4761 4762 else: 4763 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4764 4765 elif args.close_trades is not None: 4766 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4767 4768 elif args.close_all is not None: 4769 trader.CloseAll(*args.close_all) 4770 4771 elif args.limits: 4772 if args.output is not None: 4773 trader.withdrawalLimitsFile = args.output 4774 4775 trader.OverviewLimits(show=True) 4776 4777 elif args.user_info: 4778 if args.output is not None: 4779 trader.userInfoFile = args.output 4780 4781 trader.OverviewUserInfo(show=True) 4782 4783 elif args.account: 4784 if args.output is not None: 4785 trader.userAccountsFile = args.output 4786 4787 trader.OverviewAccounts(show=True) 4788 4789 else: 4790 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4791 raise Exception("There is no command to execute") 4792 4793 except Exception: 4794 trace = tb.format_exc() 4795 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4796 if e in trace: 4797 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4798 break 4799 4800 uLogger.debug(trace) 4801 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4802 exitCode = 255 # an error occurred, must be open a ticket for this issue 4803 4804 finally: 4805 finish = datetime.now(tzutc()) 4806 4807 if exitCode == 0: 4808 if args.more: 4809 uLogger.debug("All operations were finished success (summary code is 0).") 4810 4811 else: 4812 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4813 os.path.abspath(uLog.defaultLogFile), exitCode, 4814 )) 4815 4816 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4817 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4818 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4819 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4820 )) 4821 uLogger.debug("=-" * 50) 4822 4823 if not kwargs: 4824 sys.exit(exitCode) 4825 4826 else: 4827 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: